From aafdc5945a8de3899b0e0ea03b968b48b18d57f3 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 2 May 2026 18:15:21 +0100 Subject: [PATCH] chore: delete stale memory host bridges --- .../src/host/embeddings.test.ts | 122 ++- scripts/deadcode-unused-files.allowlist.mjs | 39 - scripts/test-projects.test-support.mjs | 3 +- src/memory-host-sdk/host/batch-error-utils.ts | 1 - src/memory-host-sdk/host/batch-http.test.ts | 86 --- src/memory-host-sdk/host/batch-http.ts | 37 - src/memory-host-sdk/host/batch-output.ts | 1 - .../host/batch-provider-common.ts | 12 - src/memory-host-sdk/host/batch-runner.ts | 71 -- src/memory-host-sdk/host/batch-status.ts | 1 - src/memory-host-sdk/host/batch-upload.ts | 45 -- src/memory-host-sdk/host/batch-utils.ts | 39 - .../host/embedding-chunk-limits.ts | 42 -- .../host/embedding-input-limits.ts | 1 - .../host/embedding-model-limits.ts | 16 - .../host/embedding-provider-adapter-utils.ts | 29 - src/memory-host-sdk/host/embedding-vectors.ts | 8 - src/memory-host-sdk/host/embeddings-debug.ts | 13 - .../host/embeddings-model-normalize.ts | 16 - .../host/embeddings-remote-client.ts | 41 - .../host/embeddings-remote-fetch.test.ts | 28 - .../host/embeddings-remote-fetch.ts | 27 - .../host/embeddings-remote-provider.ts | 1 - src/memory-host-sdk/host/embeddings.test.ts | 124 --- src/memory-host-sdk/host/embeddings.ts | 1 - src/memory-host-sdk/host/embeddings.types.ts | 57 -- src/memory-host-sdk/host/fs-utils.ts | 31 - src/memory-host-sdk/host/hash.ts | 5 - src/memory-host-sdk/host/internal.test.ts | 546 -------------- src/memory-host-sdk/host/internal.ts | 1 - src/memory-host-sdk/host/memory-schema.ts | 1 - src/memory-host-sdk/host/mirror.test.ts | 40 - src/memory-host-sdk/host/multimodal.ts | 1 - src/memory-host-sdk/host/node-llama.ts | 31 - src/memory-host-sdk/host/post-json.test.ts | 26 - src/memory-host-sdk/host/post-json.ts | 37 - src/memory-host-sdk/host/qmd-process.ts | 1 - .../host/qmd-query-parser.test.ts | 76 -- src/memory-host-sdk/host/qmd-query-parser.ts | 116 --- src/memory-host-sdk/host/qmd-scope.test.ts | 54 -- src/memory-host-sdk/host/qmd-scope.ts | 1 - src/memory-host-sdk/host/query-expansion.ts | 1 - src/memory-host-sdk/host/read-file-shared.ts | 1 - src/memory-host-sdk/host/read-file.ts | 1 - src/memory-host-sdk/host/remote-http.test.ts | 51 -- src/memory-host-sdk/host/remote-http.ts | 32 - src/memory-host-sdk/host/secret-input.test.ts | 30 - src/memory-host-sdk/host/secret-input.ts | 28 - .../host/session-files.test.ts | 710 ------------------ src/memory-host-sdk/host/session-files.ts | 1 - src/memory-host-sdk/host/sqlite-vec.ts | 39 - src/memory-host-sdk/host/sqlite.ts | 49 -- src/memory-host-sdk/host/status-format.ts | 45 -- test/scripts/test-projects.test.ts | 3 +- test/vitest-unit-fast-config.test.ts | 2 +- test/vitest/vitest.unit-fast-paths.mjs | 7 - 56 files changed, 121 insertions(+), 2707 deletions(-) delete mode 100644 src/memory-host-sdk/host/batch-error-utils.ts delete mode 100644 src/memory-host-sdk/host/batch-http.test.ts delete mode 100644 src/memory-host-sdk/host/batch-http.ts delete mode 100644 src/memory-host-sdk/host/batch-output.ts delete mode 100644 src/memory-host-sdk/host/batch-provider-common.ts delete mode 100644 src/memory-host-sdk/host/batch-runner.ts delete mode 100644 src/memory-host-sdk/host/batch-status.ts delete mode 100644 src/memory-host-sdk/host/batch-upload.ts delete mode 100644 src/memory-host-sdk/host/batch-utils.ts delete mode 100644 src/memory-host-sdk/host/embedding-chunk-limits.ts delete mode 100644 src/memory-host-sdk/host/embedding-input-limits.ts delete mode 100644 src/memory-host-sdk/host/embedding-model-limits.ts delete mode 100644 src/memory-host-sdk/host/embedding-provider-adapter-utils.ts delete mode 100644 src/memory-host-sdk/host/embedding-vectors.ts delete mode 100644 src/memory-host-sdk/host/embeddings-debug.ts delete mode 100644 src/memory-host-sdk/host/embeddings-model-normalize.ts delete mode 100644 src/memory-host-sdk/host/embeddings-remote-client.ts delete mode 100644 src/memory-host-sdk/host/embeddings-remote-fetch.test.ts delete mode 100644 src/memory-host-sdk/host/embeddings-remote-fetch.ts delete mode 100644 src/memory-host-sdk/host/embeddings-remote-provider.ts delete mode 100644 src/memory-host-sdk/host/embeddings.test.ts delete mode 100644 src/memory-host-sdk/host/embeddings.ts delete mode 100644 src/memory-host-sdk/host/embeddings.types.ts delete mode 100644 src/memory-host-sdk/host/fs-utils.ts delete mode 100644 src/memory-host-sdk/host/hash.ts delete mode 100644 src/memory-host-sdk/host/internal.test.ts delete mode 100644 src/memory-host-sdk/host/internal.ts delete mode 100644 src/memory-host-sdk/host/memory-schema.ts delete mode 100644 src/memory-host-sdk/host/mirror.test.ts delete mode 100644 src/memory-host-sdk/host/multimodal.ts delete mode 100644 src/memory-host-sdk/host/node-llama.ts delete mode 100644 src/memory-host-sdk/host/post-json.test.ts delete mode 100644 src/memory-host-sdk/host/post-json.ts delete mode 100644 src/memory-host-sdk/host/qmd-process.ts delete mode 100644 src/memory-host-sdk/host/qmd-query-parser.test.ts delete mode 100644 src/memory-host-sdk/host/qmd-query-parser.ts delete mode 100644 src/memory-host-sdk/host/qmd-scope.test.ts delete mode 100644 src/memory-host-sdk/host/qmd-scope.ts delete mode 100644 src/memory-host-sdk/host/query-expansion.ts delete mode 100644 src/memory-host-sdk/host/read-file-shared.ts delete mode 100644 src/memory-host-sdk/host/read-file.ts delete mode 100644 src/memory-host-sdk/host/remote-http.test.ts delete mode 100644 src/memory-host-sdk/host/remote-http.ts delete mode 100644 src/memory-host-sdk/host/secret-input.test.ts delete mode 100644 src/memory-host-sdk/host/secret-input.ts delete mode 100644 src/memory-host-sdk/host/session-files.test.ts delete mode 100644 src/memory-host-sdk/host/session-files.ts delete mode 100644 src/memory-host-sdk/host/sqlite-vec.ts delete mode 100644 src/memory-host-sdk/host/sqlite.ts delete mode 100644 src/memory-host-sdk/host/status-format.ts diff --git a/packages/memory-host-sdk/src/host/embeddings.test.ts b/packages/memory-host-sdk/src/host/embeddings.test.ts index f1e33e6acad..6c8436ec4e2 100644 --- a/packages/memory-host-sdk/src/host/embeddings.test.ts +++ b/packages/memory-host-sdk/src/host/embeddings.test.ts @@ -1,8 +1,122 @@ -import { describe, expect, it } from "vitest"; -import { DEFAULT_LOCAL_MODEL } from "./embeddings.js"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { createLocalEmbeddingProvider, DEFAULT_LOCAL_MODEL } from "./embeddings.js"; + +const nodeLlamaMock = vi.hoisted(() => ({ + importNodeLlamaCpp: vi.fn(), +})); + +vi.mock("./node-llama.js", () => ({ + importNodeLlamaCpp: nodeLlamaMock.importNodeLlamaCpp, +})); + +beforeEach(() => { + nodeLlamaMock.importNodeLlamaCpp.mockReset(); +}); + +afterEach(() => { + vi.resetAllMocks(); +}); + +function mockLocalEmbeddingRuntime(vector = new Float32Array([2.35, 3.45, 0.63, 4.3])) { + const getEmbeddingFor = vi.fn().mockResolvedValue({ vector }); + const createEmbeddingContext = vi.fn().mockResolvedValue({ getEmbeddingFor }); + const loadModel = vi.fn().mockResolvedValue({ createEmbeddingContext }); + const resolveModelFile = vi.fn(async (modelPath: string) => `/resolved/${modelPath}`); + + nodeLlamaMock.importNodeLlamaCpp.mockResolvedValue({ + getLlama: async () => ({ loadModel }), + resolveModelFile, + LlamaLogLevel: { error: 0 }, + } as never); + + return { createEmbeddingContext, getEmbeddingFor, loadModel, resolveModelFile }; +} + +describe("local embedding provider", () => { + it("normalizes local embeddings and resolves the default local model", async () => { + const runtime = mockLocalEmbeddingRuntime(); + + const provider = await createLocalEmbeddingProvider({ + config: {} as never, + provider: "local", + model: "", + fallback: "none", + }); + + const embedding = await provider.embedQuery("test query"); + const magnitude = Math.sqrt(embedding.reduce((sum, value) => sum + value * value, 0)); -describe("package embeddings barrel", () => { - it("re-exports the source local embedding contract", () => { expect(DEFAULT_LOCAL_MODEL).toContain("embeddinggemma"); + expect(magnitude).toBeCloseTo(1, 5); + expect(runtime.resolveModelFile).toHaveBeenCalledWith(DEFAULT_LOCAL_MODEL, undefined); + expect(runtime.getEmbeddingFor).toHaveBeenCalledWith("test query"); + }); + + it("passes default contextSize (4096) to createEmbeddingContext when not configured", async () => { + const runtime = mockLocalEmbeddingRuntime(); + + const provider = await createLocalEmbeddingProvider({ + config: {} as never, + provider: "local", + model: "", + fallback: "none", + }); + + await provider.embedQuery("context size default test"); + + expect(runtime.createEmbeddingContext).toHaveBeenCalledWith({ contextSize: 4096 }); + }); + + it("passes configured contextSize to createEmbeddingContext", async () => { + const runtime = mockLocalEmbeddingRuntime(); + + const provider = await createLocalEmbeddingProvider({ + config: {} as never, + provider: "local", + model: "", + fallback: "none", + local: { contextSize: 2048 }, + }); + + await provider.embedQuery("context size custom test"); + + expect(runtime.createEmbeddingContext).toHaveBeenCalledWith({ contextSize: 2048 }); + }); + + it('passes "auto" contextSize to createEmbeddingContext when explicitly set', async () => { + const runtime = mockLocalEmbeddingRuntime(); + + const provider = await createLocalEmbeddingProvider({ + config: {} as never, + provider: "local", + model: "", + fallback: "none", + local: { contextSize: "auto" }, + }); + + await provider.embedQuery("context size auto test"); + + expect(runtime.createEmbeddingContext).toHaveBeenCalledWith({ contextSize: "auto" }); + }); + + it("trims explicit local model paths and cache directories", async () => { + const runtime = mockLocalEmbeddingRuntime(new Float32Array([1, 0])); + + const provider = await createLocalEmbeddingProvider({ + config: {} as never, + provider: "local", + model: "", + fallback: "none", + local: { + modelPath: " /models/embed.gguf ", + modelCacheDir: " /cache/models ", + }, + }); + + await provider.embedBatch(["a", "b"]); + + expect(provider.model).toBe("/models/embed.gguf"); + expect(runtime.resolveModelFile).toHaveBeenCalledWith("/models/embed.gguf", "/cache/models"); + expect(runtime.getEmbeddingFor).toHaveBeenCalledTimes(2); }); }); diff --git a/scripts/deadcode-unused-files.allowlist.mjs b/scripts/deadcode-unused-files.allowlist.mjs index b8dd455d0eb..e5d4670b2f8 100644 --- a/scripts/deadcode-unused-files.allowlist.mjs +++ b/scripts/deadcode-unused-files.allowlist.mjs @@ -20,45 +20,6 @@ export const KNIP_UNUSED_FILE_ALLOWLIST = [ "src/mcp/plugin-tools-handlers.ts", "src/mcp/plugin-tools-serve.ts", "src/mcp/tools-stdio-server.ts", - "src/memory-host-sdk/host/batch-error-utils.ts", - "src/memory-host-sdk/host/batch-http.ts", - "src/memory-host-sdk/host/batch-output.ts", - "src/memory-host-sdk/host/batch-provider-common.ts", - "src/memory-host-sdk/host/batch-runner.ts", - "src/memory-host-sdk/host/batch-status.ts", - "src/memory-host-sdk/host/batch-upload.ts", - "src/memory-host-sdk/host/batch-utils.ts", - "src/memory-host-sdk/host/embedding-chunk-limits.ts", - "src/memory-host-sdk/host/embedding-input-limits.ts", - "src/memory-host-sdk/host/embedding-model-limits.ts", - "src/memory-host-sdk/host/embedding-provider-adapter-utils.ts", - "src/memory-host-sdk/host/embedding-vectors.ts", - "src/memory-host-sdk/host/embeddings-debug.ts", - "src/memory-host-sdk/host/embeddings-model-normalize.ts", - "src/memory-host-sdk/host/embeddings-remote-client.ts", - "src/memory-host-sdk/host/embeddings-remote-fetch.ts", - "src/memory-host-sdk/host/embeddings-remote-provider.ts", - "src/memory-host-sdk/host/embeddings.ts", - "src/memory-host-sdk/host/embeddings.types.ts", - "src/memory-host-sdk/host/fs-utils.ts", - "src/memory-host-sdk/host/hash.ts", - "src/memory-host-sdk/host/internal.ts", - "src/memory-host-sdk/host/memory-schema.ts", - "src/memory-host-sdk/host/multimodal.ts", - "src/memory-host-sdk/host/node-llama.ts", - "src/memory-host-sdk/host/post-json.ts", - "src/memory-host-sdk/host/qmd-process.ts", - "src/memory-host-sdk/host/qmd-query-parser.ts", - "src/memory-host-sdk/host/qmd-scope.ts", - "src/memory-host-sdk/host/query-expansion.ts", - "src/memory-host-sdk/host/read-file-shared.ts", - "src/memory-host-sdk/host/read-file.ts", - "src/memory-host-sdk/host/remote-http.ts", - "src/memory-host-sdk/host/secret-input.ts", - "src/memory-host-sdk/host/session-files.ts", - "src/memory-host-sdk/host/sqlite-vec.ts", - "src/memory-host-sdk/host/sqlite.ts", - "src/memory-host-sdk/host/status-format.ts", "src/plugins/build-smoke-entry.ts", "src/plugins/contracts/host-hook-fixture.ts", "src/plugins/contracts/rootdir-boundary-canary.ts", diff --git a/scripts/test-projects.test-support.mjs b/scripts/test-projects.test-support.mjs index deeb7064293..74749e43ed7 100644 --- a/scripts/test-projects.test-support.mjs +++ b/scripts/test-projects.test-support.mjs @@ -361,9 +361,8 @@ const SOURCE_TEST_TARGETS = new Map([ ], [ "src/memory-host-sdk/host/embedding-defaults.ts", - ["src/memory-host-sdk/host/embeddings.test.ts"], + ["packages/memory-host-sdk/src/host/embeddings.test.ts"], ], - ["src/memory-host-sdk/host/embeddings.ts", ["src/memory-host-sdk/host/embeddings.test.ts"]], [ "src/plugin-sdk/test-helpers/directory-ids.ts", [ diff --git a/src/memory-host-sdk/host/batch-error-utils.ts b/src/memory-host-sdk/host/batch-error-utils.ts deleted file mode 100644 index 28cffda44b3..00000000000 --- a/src/memory-host-sdk/host/batch-error-utils.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "../../../packages/memory-host-sdk/src/host/batch-error-utils.js"; diff --git a/src/memory-host-sdk/host/batch-http.test.ts b/src/memory-host-sdk/host/batch-http.test.ts deleted file mode 100644 index 3519a80f038..00000000000 --- a/src/memory-host-sdk/host/batch-http.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; - -vi.mock("../../infra/retry.js", () => ({ - retryAsync: vi.fn(async (run: () => Promise) => await run()), -})); - -vi.mock("./post-json.js", () => ({ - postJson: vi.fn(), -})); - -describe("postJsonWithRetry", () => { - let retryAsyncMock: ReturnType< - typeof vi.mocked - >; - let postJsonMock: ReturnType>; - let postJsonWithRetry: typeof import("./batch-http.js").postJsonWithRetry; - - beforeAll(async () => { - ({ postJsonWithRetry } = await import("./batch-http.js")); - const retryModule = await import("../../infra/retry.js"); - const postJsonModule = await import("./post-json.js"); - retryAsyncMock = vi.mocked(retryModule.retryAsync); - postJsonMock = vi.mocked(postJsonModule.postJson); - }); - - beforeEach(() => { - vi.clearAllMocks(); - }); - - it("posts JSON and returns parsed response payload", async () => { - postJsonMock.mockImplementationOnce(async (params) => { - return await params.parse({ ok: true, ids: [1, 2] }); - }); - - const result = await postJsonWithRetry<{ ok: boolean; ids: number[] }>({ - url: "https://memory.example/v1/batch", - headers: { Authorization: "Bearer test" }, - body: { chunks: ["a", "b"] }, - errorPrefix: "memory batch failed", - }); - - expect(result).toEqual({ ok: true, ids: [1, 2] }); - expect(postJsonMock).toHaveBeenCalledWith( - expect.objectContaining({ - url: "https://memory.example/v1/batch", - headers: { Authorization: "Bearer test" }, - body: { chunks: ["a", "b"] }, - errorPrefix: "memory batch failed", - attachStatus: true, - }), - ); - - const retryOptions = retryAsyncMock.mock.calls[0]?.[1] as - | { - attempts: number; - minDelayMs: number; - maxDelayMs: number; - shouldRetry: (err: unknown) => boolean; - } - | undefined; - expect(retryOptions?.attempts).toBe(3); - expect(retryOptions?.minDelayMs).toBe(300); - expect(retryOptions?.maxDelayMs).toBe(2000); - expect(retryOptions?.shouldRetry({ status: 429 })).toBe(true); - expect(retryOptions?.shouldRetry({ status: 503 })).toBe(true); - expect(retryOptions?.shouldRetry({ status: 400 })).toBe(false); - }); - - it("attaches status to non-ok errors", async () => { - postJsonMock.mockRejectedValueOnce( - Object.assign(new Error("memory batch failed: 503 backend down"), { status: 503 }), - ); - - await expect( - postJsonWithRetry({ - url: "https://memory.example/v1/batch", - headers: {}, - body: { chunks: [] }, - errorPrefix: "memory batch failed", - }), - ).rejects.toMatchObject({ - message: expect.stringContaining("memory batch failed: 503 backend down"), - status: 503, - }); - }); -}); diff --git a/src/memory-host-sdk/host/batch-http.ts b/src/memory-host-sdk/host/batch-http.ts deleted file mode 100644 index 2970161f0ba..00000000000 --- a/src/memory-host-sdk/host/batch-http.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { SsrFPolicy } from "../../infra/net/ssrf.js"; -import { retryAsync } from "../../infra/retry.js"; -import { postJson } from "./post-json.js"; - -export async function postJsonWithRetry(params: { - url: string; - headers: Record; - ssrfPolicy?: SsrFPolicy; - fetchImpl?: typeof fetch; - body: unknown; - errorPrefix: string; -}): Promise { - return await retryAsync( - async () => { - return await postJson({ - url: params.url, - headers: params.headers, - ssrfPolicy: params.ssrfPolicy, - fetchImpl: params.fetchImpl, - body: params.body, - errorPrefix: params.errorPrefix, - attachStatus: true, - parse: async (payload) => payload as T, - }); - }, - { - attempts: 3, - minDelayMs: 300, - maxDelayMs: 2000, - jitter: 0.2, - shouldRetry: (err) => { - const status = (err as { status?: number }).status; - return status === 429 || (typeof status === "number" && status >= 500); - }, - }, - ); -} diff --git a/src/memory-host-sdk/host/batch-output.ts b/src/memory-host-sdk/host/batch-output.ts deleted file mode 100644 index 2ce39fc2461..00000000000 --- a/src/memory-host-sdk/host/batch-output.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "../../../packages/memory-host-sdk/src/host/batch-output.js"; diff --git a/src/memory-host-sdk/host/batch-provider-common.ts b/src/memory-host-sdk/host/batch-provider-common.ts deleted file mode 100644 index 878387ffd6d..00000000000 --- a/src/memory-host-sdk/host/batch-provider-common.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { EmbeddingBatchOutputLine } from "./batch-output.js"; - -export type EmbeddingBatchStatus = { - id?: string; - status?: string; - output_file_id?: string | null; - error_file_id?: string | null; -}; - -export type ProviderBatchOutputLine = EmbeddingBatchOutputLine; - -export const EMBEDDING_BATCH_ENDPOINT = "/v1/embeddings"; diff --git a/src/memory-host-sdk/host/batch-runner.ts b/src/memory-host-sdk/host/batch-runner.ts deleted file mode 100644 index 7c7c40eb3b7..00000000000 --- a/src/memory-host-sdk/host/batch-runner.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { runTasksWithConcurrency } from "../../utils/run-with-concurrency.js"; -import { splitBatchRequests } from "./batch-utils.js"; - -export type EmbeddingBatchExecutionParams = { - wait: boolean; - pollIntervalMs: number; - timeoutMs: number; - concurrency: number; - debug?: (message: string, data?: Record) => void; -}; - -export async function runEmbeddingBatchGroups(params: { - requests: TRequest[]; - maxRequests: number; - wait: EmbeddingBatchExecutionParams["wait"]; - pollIntervalMs: EmbeddingBatchExecutionParams["pollIntervalMs"]; - timeoutMs: EmbeddingBatchExecutionParams["timeoutMs"]; - concurrency: EmbeddingBatchExecutionParams["concurrency"]; - debugLabel: string; - debug?: EmbeddingBatchExecutionParams["debug"]; - runGroup: (args: { - group: TRequest[]; - groupIndex: number; - groups: number; - byCustomId: Map; - }) => Promise; -}): Promise> { - if (params.requests.length === 0) { - return new Map(); - } - const groups = splitBatchRequests(params.requests, params.maxRequests); - const byCustomId = new Map(); - const tasks = groups.map((group, groupIndex) => async () => { - await params.runGroup({ group, groupIndex, groups: groups.length, byCustomId }); - }); - - params.debug?.(params.debugLabel, { - requests: params.requests.length, - groups: groups.length, - wait: params.wait, - concurrency: params.concurrency, - pollIntervalMs: params.pollIntervalMs, - timeoutMs: params.timeoutMs, - }); - - const { firstError, hasError } = await runTasksWithConcurrency({ - tasks, - limit: params.concurrency, - errorMode: "stop", - }); - if (hasError) { - throw firstError; - } - return byCustomId; -} - -export function buildEmbeddingBatchGroupOptions( - params: { requests: TRequest[] } & EmbeddingBatchExecutionParams, - options: { maxRequests: number; debugLabel: string }, -) { - return { - requests: params.requests, - maxRequests: options.maxRequests, - wait: params.wait, - pollIntervalMs: params.pollIntervalMs, - timeoutMs: params.timeoutMs, - concurrency: params.concurrency, - debug: params.debug, - debugLabel: options.debugLabel, - }; -} diff --git a/src/memory-host-sdk/host/batch-status.ts b/src/memory-host-sdk/host/batch-status.ts deleted file mode 100644 index fa25fbc06b4..00000000000 --- a/src/memory-host-sdk/host/batch-status.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "../../../packages/memory-host-sdk/src/host/batch-status.js"; diff --git a/src/memory-host-sdk/host/batch-upload.ts b/src/memory-host-sdk/host/batch-upload.ts deleted file mode 100644 index 1b64202f9b6..00000000000 --- a/src/memory-host-sdk/host/batch-upload.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { - buildBatchHeaders, - normalizeBatchBaseUrl, - type BatchHttpClientConfig, -} from "./batch-utils.js"; -import { hashText } from "./hash.js"; -import { withRemoteHttpResponse } from "./remote-http.js"; - -export async function uploadBatchJsonlFile(params: { - client: BatchHttpClientConfig; - requests: unknown[]; - errorPrefix: string; -}): Promise { - const baseUrl = normalizeBatchBaseUrl(params.client); - const jsonl = params.requests.map((request) => JSON.stringify(request)).join("\n"); - const form = new FormData(); - form.append("purpose", "batch"); - form.append( - "file", - new Blob([jsonl], { type: "application/jsonl" }), - `memory-embeddings.${hashText(String(Date.now()))}.jsonl`, - ); - - const filePayload = await withRemoteHttpResponse({ - url: `${baseUrl}/files`, - ssrfPolicy: params.client.ssrfPolicy, - fetchImpl: params.client.fetchImpl, - init: { - method: "POST", - headers: buildBatchHeaders(params.client, { json: false }), - body: form, - }, - onResponse: async (fileRes) => { - if (!fileRes.ok) { - const text = await fileRes.text(); - throw new Error(`${params.errorPrefix}: ${fileRes.status} ${text}`); - } - return (await fileRes.json()) as { id?: string }; - }, - }); - if (!filePayload.id) { - throw new Error(`${params.errorPrefix}: missing file id`); - } - return filePayload.id; -} diff --git a/src/memory-host-sdk/host/batch-utils.ts b/src/memory-host-sdk/host/batch-utils.ts deleted file mode 100644 index d5d293914fe..00000000000 --- a/src/memory-host-sdk/host/batch-utils.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { SsrFPolicy } from "../../infra/net/ssrf.js"; - -export type BatchHttpClientConfig = { - baseUrl?: string; - headers?: Record; - ssrfPolicy?: SsrFPolicy; - fetchImpl?: typeof fetch; -}; - -export function normalizeBatchBaseUrl(client: BatchHttpClientConfig): string { - return client.baseUrl?.replace(/\/$/, "") ?? ""; -} - -export function buildBatchHeaders( - client: Pick, - params: { json: boolean }, -): Record { - const headers = client.headers ? { ...client.headers } : {}; - if (params.json) { - if (!headers["Content-Type"] && !headers["content-type"]) { - headers["Content-Type"] = "application/json"; - } - } else { - delete headers["Content-Type"]; - delete headers["content-type"]; - } - return headers; -} - -export function splitBatchRequests(requests: T[], maxRequests: number): T[][] { - if (requests.length <= maxRequests) { - return [requests]; - } - const groups: T[][] = []; - for (let i = 0; i < requests.length; i += maxRequests) { - groups.push(requests.slice(i, i + maxRequests)); - } - return groups; -} diff --git a/src/memory-host-sdk/host/embedding-chunk-limits.ts b/src/memory-host-sdk/host/embedding-chunk-limits.ts deleted file mode 100644 index 0156b97ef77..00000000000 --- a/src/memory-host-sdk/host/embedding-chunk-limits.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { estimateUtf8Bytes, splitTextToUtf8ByteLimit } from "./embedding-input-limits.js"; -import { hasNonTextEmbeddingParts } from "./embedding-inputs.js"; -import { resolveEmbeddingMaxInputTokens } from "./embedding-model-limits.js"; -import type { EmbeddingProvider } from "./embeddings.js"; -import { hashText } from "./hash.js"; -import type { MemoryChunk } from "./internal.js"; - -export function enforceEmbeddingMaxInputTokens( - provider: EmbeddingProvider, - chunks: MemoryChunk[], - hardMaxInputTokens?: number, -): MemoryChunk[] { - const providerMaxInputTokens = resolveEmbeddingMaxInputTokens(provider); - const maxInputTokens = - typeof hardMaxInputTokens === "number" && hardMaxInputTokens > 0 - ? Math.min(providerMaxInputTokens, hardMaxInputTokens) - : providerMaxInputTokens; - const out: MemoryChunk[] = []; - - for (const chunk of chunks) { - if (hasNonTextEmbeddingParts(chunk.embeddingInput)) { - out.push(chunk); - continue; - } - if (estimateUtf8Bytes(chunk.text) <= maxInputTokens) { - out.push(chunk); - continue; - } - - for (const text of splitTextToUtf8ByteLimit(chunk.text, maxInputTokens)) { - out.push({ - startLine: chunk.startLine, - endLine: chunk.endLine, - text, - hash: hashText(text), - embeddingInput: { text }, - }); - } - } - - return out; -} diff --git a/src/memory-host-sdk/host/embedding-input-limits.ts b/src/memory-host-sdk/host/embedding-input-limits.ts deleted file mode 100644 index 406864a58c4..00000000000 --- a/src/memory-host-sdk/host/embedding-input-limits.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "../../../packages/memory-host-sdk/src/host/embedding-input-limits.js"; diff --git a/src/memory-host-sdk/host/embedding-model-limits.ts b/src/memory-host-sdk/host/embedding-model-limits.ts deleted file mode 100644 index 714114f670e..00000000000 --- a/src/memory-host-sdk/host/embedding-model-limits.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { EmbeddingProvider } from "./embeddings.js"; - -const DEFAULT_EMBEDDING_MAX_INPUT_TOKENS = 8192; -const DEFAULT_LOCAL_EMBEDDING_MAX_INPUT_TOKENS = 2048; - -export function resolveEmbeddingMaxInputTokens(provider: EmbeddingProvider): number { - if (typeof provider.maxInputTokens === "number") { - return provider.maxInputTokens; - } - - if (provider.id === "local") { - return DEFAULT_LOCAL_EMBEDDING_MAX_INPUT_TOKENS; - } - - return DEFAULT_EMBEDDING_MAX_INPUT_TOKENS; -} diff --git a/src/memory-host-sdk/host/embedding-provider-adapter-utils.ts b/src/memory-host-sdk/host/embedding-provider-adapter-utils.ts deleted file mode 100644 index 401173b2826..00000000000 --- a/src/memory-host-sdk/host/embedding-provider-adapter-utils.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; - -export function isMissingEmbeddingApiKeyError(err: unknown): boolean { - return err instanceof Error && err.message.includes("No API key found for provider"); -} - -export function sanitizeEmbeddingCacheHeaders( - headers: Record, - excludedHeaderNames: string[], -): Array<[string, string]> { - const excluded = new Set( - excludedHeaderNames.map((name) => normalizeLowercaseStringOrEmpty(name)), - ); - return Object.entries(headers) - .filter(([key]) => !excluded.has(normalizeLowercaseStringOrEmpty(key))) - .toSorted(([a], [b]) => a.localeCompare(b)) - .map(([key, value]) => [key, value]); -} - -export function mapBatchEmbeddingsByIndex( - byCustomId: Map, - count: number, -): number[][] { - const embeddings: number[][] = []; - for (let index = 0; index < count; index += 1) { - embeddings.push(byCustomId.get(String(index)) ?? []); - } - return embeddings; -} diff --git a/src/memory-host-sdk/host/embedding-vectors.ts b/src/memory-host-sdk/host/embedding-vectors.ts deleted file mode 100644 index d589f61390d..00000000000 --- a/src/memory-host-sdk/host/embedding-vectors.ts +++ /dev/null @@ -1,8 +0,0 @@ -export function sanitizeAndNormalizeEmbedding(vec: number[]): number[] { - const sanitized = vec.map((value) => (Number.isFinite(value) ? value : 0)); - const magnitude = Math.sqrt(sanitized.reduce((sum, value) => sum + value * value, 0)); - if (magnitude < 1e-10) { - return sanitized; - } - return sanitized.map((value) => value / magnitude); -} diff --git a/src/memory-host-sdk/host/embeddings-debug.ts b/src/memory-host-sdk/host/embeddings-debug.ts deleted file mode 100644 index a9f20d55e8a..00000000000 --- a/src/memory-host-sdk/host/embeddings-debug.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { isTruthyEnvValue } from "../../infra/env.js"; -import { createSubsystemLogger } from "../../logging/subsystem.js"; - -const debugEmbeddings = isTruthyEnvValue(process.env.OPENCLAW_DEBUG_MEMORY_EMBEDDINGS); -const log = createSubsystemLogger("memory/embeddings"); - -export function debugEmbeddingsLog(message: string, meta?: Record): void { - if (!debugEmbeddings) { - return; - } - const suffix = meta ? ` ${JSON.stringify(meta)}` : ""; - log.raw(`${message}${suffix}`); -} diff --git a/src/memory-host-sdk/host/embeddings-model-normalize.ts b/src/memory-host-sdk/host/embeddings-model-normalize.ts deleted file mode 100644 index 85fcf5b16ce..00000000000 --- a/src/memory-host-sdk/host/embeddings-model-normalize.ts +++ /dev/null @@ -1,16 +0,0 @@ -export function normalizeEmbeddingModelWithPrefixes(params: { - model: string; - defaultModel: string; - prefixes: string[]; -}): string { - const trimmed = params.model.trim(); - if (!trimmed) { - return params.defaultModel; - } - for (const prefix of params.prefixes) { - if (trimmed.startsWith(prefix)) { - return trimmed.slice(prefix.length); - } - } - return trimmed; -} diff --git a/src/memory-host-sdk/host/embeddings-remote-client.ts b/src/memory-host-sdk/host/embeddings-remote-client.ts deleted file mode 100644 index 77f11479f65..00000000000 --- a/src/memory-host-sdk/host/embeddings-remote-client.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { requireApiKey, resolveApiKeyForProvider } from "../../agents/model-auth.js"; -import type { SsrFPolicy } from "../../infra/net/ssrf.js"; -import { normalizeOptionalString } from "../../shared/string-coerce.js"; -import type { EmbeddingProviderOptions } from "./embeddings.types.js"; -import { buildRemoteBaseUrlPolicy } from "./remote-http.js"; -import { resolveMemorySecretInputString } from "./secret-input.js"; - -export type RemoteEmbeddingProviderId = string; - -export async function resolveRemoteEmbeddingBearerClient(params: { - provider: RemoteEmbeddingProviderId; - options: EmbeddingProviderOptions; - defaultBaseUrl: string; -}): Promise<{ baseUrl: string; headers: Record; ssrfPolicy?: SsrFPolicy }> { - const remote = params.options.remote; - const remoteApiKey = resolveMemorySecretInputString({ - value: remote?.apiKey, - path: "agents.*.memorySearch.remote.apiKey", - }); - const remoteBaseUrl = normalizeOptionalString(remote?.baseUrl); - const providerConfig = params.options.config.models?.providers?.[params.provider]; - const apiKey = remoteApiKey - ? remoteApiKey - : requireApiKey( - await resolveApiKeyForProvider({ - provider: params.provider, - cfg: params.options.config, - agentDir: params.options.agentDir, - }), - params.provider, - ); - const baseUrl = - remoteBaseUrl || normalizeOptionalString(providerConfig?.baseUrl) || params.defaultBaseUrl; - const headerOverrides = Object.assign({}, providerConfig?.headers, remote?.headers); - const headers: Record = { - "Content-Type": "application/json", - Authorization: `Bearer ${apiKey}`, - ...headerOverrides, - }; - return { baseUrl, headers, ssrfPolicy: buildRemoteBaseUrlPolicy(baseUrl) }; -} diff --git a/src/memory-host-sdk/host/embeddings-remote-fetch.test.ts b/src/memory-host-sdk/host/embeddings-remote-fetch.test.ts deleted file mode 100644 index cf8fa4d5c3e..00000000000 --- a/src/memory-host-sdk/host/embeddings-remote-fetch.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import { fetchRemoteEmbeddingVectors } from "./embeddings-remote-fetch.js"; - -describe("fetchRemoteEmbeddingVectors", () => { - it("maps remote embedding response data through an injected fetch", async () => { - const fetchImpl = vi.fn( - async () => - new Response( - JSON.stringify({ data: [{ embedding: [0.1, 0.2] }, {}, { embedding: [0.3] }] }), - { - status: 200, - headers: { "content-type": "application/json" }, - }, - ), - ) as typeof fetch; - - const vectors = await fetchRemoteEmbeddingVectors({ - url: "https://example.com/v1/embeddings", - headers: { Authorization: "Bearer test" }, - ssrfPolicy: { allowedHostnames: ["example.com"] }, - fetchImpl, - body: { input: ["one", "two", "three"] }, - errorPrefix: "embedding fetch failed", - }); - - expect(vectors).toEqual([[0.1, 0.2], [], [0.3]]); - }); -}); diff --git a/src/memory-host-sdk/host/embeddings-remote-fetch.ts b/src/memory-host-sdk/host/embeddings-remote-fetch.ts deleted file mode 100644 index dae42454409..00000000000 --- a/src/memory-host-sdk/host/embeddings-remote-fetch.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { SsrFPolicy } from "../../infra/net/ssrf.js"; -import { postJson } from "./post-json.js"; - -export async function fetchRemoteEmbeddingVectors(params: { - url: string; - headers: Record; - ssrfPolicy?: SsrFPolicy; - fetchImpl?: typeof fetch; - body: unknown; - errorPrefix: string; -}): Promise { - return await postJson({ - url: params.url, - headers: params.headers, - ssrfPolicy: params.ssrfPolicy, - fetchImpl: params.fetchImpl, - body: params.body, - errorPrefix: params.errorPrefix, - parse: (payload) => { - const typedPayload = payload as { - data?: Array<{ embedding?: number[] }>; - }; - const data = typedPayload.data ?? []; - return data.map((entry) => entry.embedding ?? []); - }, - }); -} diff --git a/src/memory-host-sdk/host/embeddings-remote-provider.ts b/src/memory-host-sdk/host/embeddings-remote-provider.ts deleted file mode 100644 index 2be2fb5f9e0..00000000000 --- a/src/memory-host-sdk/host/embeddings-remote-provider.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "../../../packages/memory-host-sdk/src/host/embeddings-remote-provider.js"; diff --git a/src/memory-host-sdk/host/embeddings.test.ts b/src/memory-host-sdk/host/embeddings.test.ts deleted file mode 100644 index 03bcd509887..00000000000 --- a/src/memory-host-sdk/host/embeddings.test.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { createLocalEmbeddingProvider, DEFAULT_LOCAL_MODEL } from "./embeddings.js"; - -const nodeLlamaMock = vi.hoisted(() => ({ - importNodeLlamaCpp: vi.fn(), -})); - -vi.mock("../../../packages/memory-host-sdk/src/host/node-llama.js", () => ({ - importNodeLlamaCpp: nodeLlamaMock.importNodeLlamaCpp, -})); -vi.mock("./node-llama.js", () => ({ - importNodeLlamaCpp: nodeLlamaMock.importNodeLlamaCpp, -})); - -beforeEach(() => { - nodeLlamaMock.importNodeLlamaCpp.mockReset(); -}); - -afterEach(() => { - vi.resetAllMocks(); -}); - -function mockLocalEmbeddingRuntime(vector = new Float32Array([2.35, 3.45, 0.63, 4.3])) { - const getEmbeddingFor = vi.fn().mockResolvedValue({ vector }); - const createEmbeddingContext = vi.fn().mockResolvedValue({ getEmbeddingFor }); - const loadModel = vi.fn().mockResolvedValue({ createEmbeddingContext }); - const resolveModelFile = vi.fn(async (modelPath: string) => `/resolved/${modelPath}`); - - nodeLlamaMock.importNodeLlamaCpp.mockResolvedValue({ - getLlama: async () => ({ loadModel }), - resolveModelFile, - LlamaLogLevel: { error: 0 }, - } as never); - - return { createEmbeddingContext, getEmbeddingFor, loadModel, resolveModelFile }; -} - -describe("local embedding provider", () => { - it("normalizes local embeddings and resolves the default local model", async () => { - const runtime = mockLocalEmbeddingRuntime(); - - const provider = await createLocalEmbeddingProvider({ - config: {} as never, - provider: "local", - model: "", - fallback: "none", - }); - - const embedding = await provider.embedQuery("test query"); - const magnitude = Math.sqrt(embedding.reduce((sum, value) => sum + value * value, 0)); - - expect(magnitude).toBeCloseTo(1, 5); - expect(runtime.resolveModelFile).toHaveBeenCalledWith(DEFAULT_LOCAL_MODEL, undefined); - expect(runtime.getEmbeddingFor).toHaveBeenCalledWith("test query"); - }); - - it("passes default contextSize (4096) to createEmbeddingContext when not configured", async () => { - const runtime = mockLocalEmbeddingRuntime(); - - const provider = await createLocalEmbeddingProvider({ - config: {} as never, - provider: "local", - model: "", - fallback: "none", - }); - - await provider.embedQuery("context size default test"); - - expect(runtime.createEmbeddingContext).toHaveBeenCalledWith({ contextSize: 4096 }); - }); - - it("passes configured contextSize to createEmbeddingContext", async () => { - const runtime = mockLocalEmbeddingRuntime(); - - const provider = await createLocalEmbeddingProvider({ - config: {} as never, - provider: "local", - model: "", - fallback: "none", - local: { contextSize: 2048 }, - }); - - await provider.embedQuery("context size custom test"); - - expect(runtime.createEmbeddingContext).toHaveBeenCalledWith({ contextSize: 2048 }); - }); - - it('passes "auto" contextSize to createEmbeddingContext when explicitly set', async () => { - const runtime = mockLocalEmbeddingRuntime(); - - const provider = await createLocalEmbeddingProvider({ - config: {} as never, - provider: "local", - model: "", - fallback: "none", - local: { contextSize: "auto" }, - }); - - await provider.embedQuery("context size auto test"); - - expect(runtime.createEmbeddingContext).toHaveBeenCalledWith({ contextSize: "auto" }); - }); - - it("trims explicit local model paths and cache directories", async () => { - const runtime = mockLocalEmbeddingRuntime(new Float32Array([1, 0])); - - const provider = await createLocalEmbeddingProvider({ - config: {} as never, - provider: "local", - model: "", - fallback: "none", - local: { - modelPath: " /models/embed.gguf ", - modelCacheDir: " /cache/models ", - }, - }); - - await provider.embedBatch(["a", "b"]); - - expect(provider.model).toBe("/models/embed.gguf"); - expect(runtime.resolveModelFile).toHaveBeenCalledWith("/models/embed.gguf", "/cache/models"); - expect(runtime.getEmbeddingFor).toHaveBeenCalledTimes(2); - }); -}); diff --git a/src/memory-host-sdk/host/embeddings.ts b/src/memory-host-sdk/host/embeddings.ts deleted file mode 100644 index a56516b225c..00000000000 --- a/src/memory-host-sdk/host/embeddings.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "../../../packages/memory-host-sdk/src/host/embeddings.js"; diff --git a/src/memory-host-sdk/host/embeddings.types.ts b/src/memory-host-sdk/host/embeddings.types.ts deleted file mode 100644 index ce906c3a066..00000000000 --- a/src/memory-host-sdk/host/embeddings.types.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { OpenClawConfig } from "../../config/types.openclaw.js"; -import type { SecretInput } from "../../config/types.secrets.js"; -import type { EmbeddingInput } from "./embedding-inputs.js"; - -export type EmbeddingProvider = { - id: string; - model: string; - maxInputTokens?: number; - embedQuery: (text: string) => Promise; - embedBatch: (texts: string[]) => Promise; - embedBatchInputs?: (inputs: EmbeddingInput[]) => Promise; -}; - -export type EmbeddingProviderId = string; -export type EmbeddingProviderRequest = string; -export type EmbeddingProviderFallback = string; - -export type GeminiTaskType = - | "RETRIEVAL_QUERY" - | "RETRIEVAL_DOCUMENT" - | "SEMANTIC_SIMILARITY" - | "CLASSIFICATION" - | "CLUSTERING" - | "QUESTION_ANSWERING" - | "FACT_VERIFICATION"; - -export type EmbeddingProviderOptions = { - config: OpenClawConfig; - agentDir?: string; - provider?: EmbeddingProviderRequest; - remote?: { - baseUrl?: string; - apiKey?: SecretInput; - headers?: Record; - }; - model: string; - inputType?: string; - queryInputType?: string; - documentInputType?: string; - fallback?: EmbeddingProviderFallback; - local?: { - modelPath?: string; - modelCacheDir?: string; - /** - * Context size passed to node-llama-cpp `createEmbeddingContext`. - * Default: 4096, chosen to cover typical memory-search chunks (128–512 tokens) - * while keeping non-weight VRAM bounded. - * Set `"auto"` to let node-llama-cpp use the model's trained maximum — not - * recommended for 8B+ models (e.g. Qwen3-Embedding-8B: up to 40 960 tokens → ~32 GB VRAM). - */ - contextSize?: number | "auto"; - }; - /** Provider-specific output vector dimensions for supported embedding families. */ - outputDimensionality?: number; - /** Gemini: override the default task type sent with embedding requests. */ - taskType?: GeminiTaskType; -}; diff --git a/src/memory-host-sdk/host/fs-utils.ts b/src/memory-host-sdk/host/fs-utils.ts deleted file mode 100644 index 81107c7ef3d..00000000000 --- a/src/memory-host-sdk/host/fs-utils.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { Stats } from "node:fs"; -import fs from "node:fs/promises"; - -export type RegularFileStatResult = { missing: true } | { missing: false; stat: Stats }; - -export function isFileMissingError( - err: unknown, -): err is NodeJS.ErrnoException & { code: "ENOENT" } { - return Boolean( - err && - typeof err === "object" && - "code" in err && - (err as Partial).code === "ENOENT", - ); -} - -export async function statRegularFile(absPath: string): Promise { - let stat: Stats; - try { - stat = await fs.lstat(absPath); - } catch (err) { - if (isFileMissingError(err)) { - return { missing: true }; - } - throw err; - } - if (stat.isSymbolicLink() || !stat.isFile()) { - throw new Error("path required"); - } - return { missing: false, stat }; -} diff --git a/src/memory-host-sdk/host/hash.ts b/src/memory-host-sdk/host/hash.ts deleted file mode 100644 index 458a6b48ed2..00000000000 --- a/src/memory-host-sdk/host/hash.ts +++ /dev/null @@ -1,5 +0,0 @@ -import crypto from "node:crypto"; - -export function hashText(value: string): string { - return crypto.createHash("sha256").update(value).digest("hex"); -} diff --git a/src/memory-host-sdk/host/internal.test.ts b/src/memory-host-sdk/host/internal.test.ts deleted file mode 100644 index 8e0e3d7773e..00000000000 --- a/src/memory-host-sdk/host/internal.test.ts +++ /dev/null @@ -1,546 +0,0 @@ -import fsSync from "node:fs"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; -import { afterAll, beforeEach, describe, expect, it, vi } from "vitest"; - -vi.mock("../../media/mime.js", () => ({ - detectMime: async (opts: { filePath?: string }) => { - if (opts.filePath?.endsWith(".png")) { - return "image/png"; - } - if (opts.filePath?.endsWith(".wav")) { - return "audio/wav"; - } - return undefined; - }, -})); - -import { - buildMultimodalChunkForIndexing, - buildFileEntry, - chunkMarkdown, - isMemoryPath, - listMemoryFiles, - normalizeExtraMemoryPaths, - remapChunkLines, -} from "./internal.js"; -import { - DEFAULT_MEMORY_MULTIMODAL_MAX_FILE_BYTES, - type MemoryMultimodalSettings, -} from "./multimodal.js"; - -const sharedTempRoot = fsSync.mkdtempSync(path.join(os.tmpdir(), "memory-host-sdk-tests-")); -let sharedTempId = 0; - -afterAll(() => { - fsSync.rmSync(sharedTempRoot, { recursive: true, force: true }); -}); - -function setupTempDirLifecycle(prefix: string): () => string { - let tmpDir = ""; - beforeEach(() => { - tmpDir = path.join(sharedTempRoot, `${prefix}${sharedTempId++}`); - fsSync.mkdirSync(tmpDir, { recursive: true }); - }); - return () => tmpDir; -} - -describe("normalizeExtraMemoryPaths", () => { - it("trims, resolves, and dedupes paths", () => { - const workspaceDir = path.join(os.tmpdir(), "memory-test-workspace"); - const absPath = path.resolve(path.sep, "shared-notes"); - const result = normalizeExtraMemoryPaths(workspaceDir, [ - " notes ", - "./notes", - absPath, - absPath, - "", - ]); - expect(result).toEqual([path.resolve(workspaceDir, "notes"), absPath]); - }); -}); - -describe("listMemoryFiles", () => { - const getTmpDir = setupTempDirLifecycle("memory-test-"); - const multimodal: MemoryMultimodalSettings = { - enabled: true, - modalities: ["image", "audio"], - maxFileBytes: DEFAULT_MEMORY_MULTIMODAL_MAX_FILE_BYTES, - }; - - it("includes files from additional paths (directory)", async () => { - const tmpDir = getTmpDir(); - fsSync.writeFileSync(path.join(tmpDir, "MEMORY.md"), "# Default memory"); - const extraDir = path.join(tmpDir, "extra-notes"); - fsSync.mkdirSync(extraDir, { recursive: true }); - fsSync.writeFileSync(path.join(extraDir, "note1.md"), "# Note 1"); - fsSync.writeFileSync(path.join(extraDir, "note2.md"), "# Note 2"); - fsSync.writeFileSync(path.join(extraDir, "ignore.txt"), "Not a markdown file"); - - const files = await listMemoryFiles(tmpDir, [extraDir]); - expect(files).toHaveLength(3); - expect(files.some((file) => file.endsWith("MEMORY.md"))).toBe(true); - expect(files.some((file) => file.endsWith("note1.md"))).toBe(true); - expect(files.some((file) => file.endsWith("note2.md"))).toBe(true); - expect(files.some((file) => file.endsWith("ignore.txt"))).toBe(false); - }); - - it("includes files from additional paths (single file)", async () => { - const tmpDir = getTmpDir(); - fsSync.writeFileSync(path.join(tmpDir, "MEMORY.md"), "# Default memory"); - const singleFile = path.join(tmpDir, "standalone.md"); - fsSync.writeFileSync(singleFile, "# Standalone"); - - const files = await listMemoryFiles(tmpDir, [singleFile]); - expect(files).toHaveLength(2); - expect(files.some((file) => file.endsWith("standalone.md"))).toBe(true); - }); - - it("ignores lowercase root memory.md when canonical MEMORY.md is absent", async () => { - const tmpDir = getTmpDir(); - fsSync.writeFileSync(path.join(tmpDir, "memory.md"), "# Legacy memory"); - - const files = await listMemoryFiles(tmpDir, [path.join(tmpDir, "memory.md")]); - - expect(files).toEqual([]); - }); - - it("prefers canonical MEMORY.md over legacy root memory.md even through extra paths", async () => { - const tmpDir = getTmpDir(); - const canonicalPath = path.join(tmpDir, "MEMORY.md"); - const legacyPath = path.join(tmpDir, "memory.md"); - const actualLstat = fs.lstat.bind(fs); - const actualReaddir = fs.readdir.bind(fs); - const lstatSpy = vi.spyOn(fs, "lstat").mockImplementation(async (target) => { - if (target === canonicalPath || target === legacyPath) { - return { - isSymbolicLink: () => false, - isFile: () => true, - isDirectory: () => false, - } as Awaited>; - } - return actualLstat(target); - }); - const readdirSpy = vi.spyOn(fs, "readdir").mockImplementation((async ( - target: unknown, - options: unknown, - ) => { - if ( - target === tmpDir && - typeof options === "object" && - options !== null && - "withFileTypes" in options && - options.withFileTypes - ) { - return [ - { - name: "MEMORY.md", - isSymbolicLink: () => false, - isDirectory: () => false, - isFile: () => true, - }, - { - name: "memory.md", - isSymbolicLink: () => false, - isDirectory: () => false, - isFile: () => true, - }, - ] as unknown as Awaited>; - } - return actualReaddir(target as never, options as never); - }) as never); - - try { - const files = await listMemoryFiles(tmpDir, [legacyPath, path.join(tmpDir, ".")]); - expect(files).toEqual([canonicalPath]); - } finally { - lstatSpy.mockRestore(); - readdirSpy.mockRestore(); - } - }); - - it("skips root-memory repair backups from workspace and explicit extra paths", async () => { - for (const testCase of [ - { - name: "workspace extra path", - extraPaths: (tmpDir: string) => [tmpDir], - }, - { - name: "explicit repair root", - extraPaths: (tmpDir: string) => [path.join(tmpDir, ".openclaw-repair", "root-memory")], - }, - ] as const) { - const tmpDir = getTmpDir(); - fsSync.writeFileSync(path.join(tmpDir, "MEMORY.md"), "# Default memory"); - const repairDir = path.join(tmpDir, ".openclaw-repair", "root-memory", "2026-04-23"); - fsSync.mkdirSync(repairDir, { recursive: true }); - fsSync.writeFileSync(path.join(repairDir, "memory.md"), "# Archived legacy memory"); - - const files = await listMemoryFiles(tmpDir, testCase.extraPaths(tmpDir)); - - expect(files, testCase.name).toHaveLength(1); - expect(files[0], testCase.name).toBe(path.join(tmpDir, "MEMORY.md")); - } - }); - - it("handles relative paths in additional paths", async () => { - const tmpDir = getTmpDir(); - fsSync.writeFileSync(path.join(tmpDir, "MEMORY.md"), "# Default memory"); - const extraDir = path.join(tmpDir, "subdir"); - fsSync.mkdirSync(extraDir, { recursive: true }); - fsSync.writeFileSync(path.join(extraDir, "nested.md"), "# Nested"); - - const files = await listMemoryFiles(tmpDir, ["subdir"]); - expect(files).toHaveLength(2); - expect(files.some((file) => file.endsWith("nested.md"))).toBe(true); - }); - - it("ignores non-existent additional paths", async () => { - const tmpDir = getTmpDir(); - fsSync.writeFileSync(path.join(tmpDir, "MEMORY.md"), "# Default memory"); - - const files = await listMemoryFiles(tmpDir, ["/does/not/exist"]); - expect(files).toHaveLength(1); - }); - - it("ignores symlinked files and directories", async () => { - const tmpDir = getTmpDir(); - fsSync.writeFileSync(path.join(tmpDir, "MEMORY.md"), "# Default memory"); - const extraDir = path.join(tmpDir, "extra"); - fsSync.mkdirSync(extraDir, { recursive: true }); - fsSync.writeFileSync(path.join(extraDir, "note.md"), "# Note"); - - const targetFile = path.join(tmpDir, "target.md"); - fsSync.writeFileSync(targetFile, "# Target"); - const linkFile = path.join(extraDir, "linked.md"); - - const targetDir = path.join(tmpDir, "target-dir"); - fsSync.mkdirSync(targetDir, { recursive: true }); - fsSync.writeFileSync(path.join(targetDir, "nested.md"), "# Nested"); - const linkDir = path.join(tmpDir, "linked-dir"); - - let symlinksOk = true; - try { - fsSync.symlinkSync(targetFile, linkFile, "file"); - fsSync.symlinkSync(targetDir, linkDir, "dir"); - } catch (err) { - const code = (err as NodeJS.ErrnoException).code; - if (code === "EPERM" || code === "EACCES") { - symlinksOk = false; - } else { - throw err; - } - } - - const files = await listMemoryFiles(tmpDir, [extraDir, linkDir]); - expect(files.some((file) => file.endsWith("note.md"))).toBe(true); - if (symlinksOk) { - expect(files.some((file) => file.endsWith("linked.md"))).toBe(false); - expect(files.some((file) => file.endsWith("nested.md"))).toBe(false); - } - }); - - it("dedupes overlapping extra paths that resolve to the same file", async () => { - const tmpDir = getTmpDir(); - fsSync.writeFileSync(path.join(tmpDir, "MEMORY.md"), "# Default memory"); - const files = await listMemoryFiles(tmpDir, [tmpDir, ".", path.join(tmpDir, "MEMORY.md")]); - const memoryMatches = files.filter((file) => file.endsWith("MEMORY.md")); - expect(memoryMatches).toHaveLength(1); - }); - - it("includes image and audio files from extra paths when multimodal is enabled", async () => { - const tmpDir = getTmpDir(); - const extraDir = path.join(tmpDir, "media"); - fsSync.mkdirSync(extraDir, { recursive: true }); - fsSync.writeFileSync(path.join(extraDir, "diagram.png"), Buffer.from("png")); - fsSync.writeFileSync(path.join(extraDir, "note.wav"), Buffer.from("wav")); - fsSync.writeFileSync(path.join(extraDir, "ignore.bin"), Buffer.from("bin")); - - const files = await listMemoryFiles(tmpDir, [extraDir], multimodal); - expect(files.some((file) => file.endsWith("diagram.png"))).toBe(true); - expect(files.some((file) => file.endsWith("note.wav"))).toBe(true); - expect(files.some((file) => file.endsWith("ignore.bin"))).toBe(false); - }); -}); - -describe("isMemoryPath", () => { - it("allows explicit access to top-level DREAMS.md", () => { - expect(isMemoryPath("DREAMS.md")).toBe(true); - }); -}); - -describe("buildFileEntry", () => { - const getTmpDir = setupTempDirLifecycle("memory-build-entry-"); - const multimodal: MemoryMultimodalSettings = { - enabled: true, - modalities: ["image", "audio"], - maxFileBytes: DEFAULT_MEMORY_MULTIMODAL_MAX_FILE_BYTES, - }; - - it("returns null when the file disappears before reading", async () => { - const tmpDir = getTmpDir(); - const target = path.join(tmpDir, "ghost.md"); - fsSync.writeFileSync(target, "ghost", "utf-8"); - fsSync.rmSync(target); - const entry = await buildFileEntry(target, tmpDir); - expect(entry).toBeNull(); - }); - - it("returns metadata when the file exists", async () => { - const tmpDir = getTmpDir(); - const target = path.join(tmpDir, "note.md"); - fsSync.writeFileSync(target, "hello", "utf-8"); - const entry = await buildFileEntry(target, tmpDir); - expect(entry).not.toBeNull(); - expect(entry?.path).toBe("note.md"); - expect(entry?.size).toBeGreaterThan(0); - }); - - it("returns multimodal metadata for eligible image files", async () => { - const tmpDir = getTmpDir(); - const target = path.join(tmpDir, "diagram.png"); - fsSync.writeFileSync(target, Buffer.from("png")); - - const entry = await buildFileEntry(target, tmpDir, multimodal); - - expect(entry).toMatchObject({ - path: "diagram.png", - kind: "multimodal", - modality: "image", - mimeType: "image/png", - contentText: "Image file: diagram.png", - }); - }); - - it("builds a multimodal chunk lazily for indexing", async () => { - const tmpDir = getTmpDir(); - const target = path.join(tmpDir, "diagram.png"); - fsSync.writeFileSync(target, Buffer.from("png")); - - const entry = await buildFileEntry(target, tmpDir, multimodal); - const built = await buildMultimodalChunkForIndexing(entry!); - - expect(built?.chunk.embeddingInput?.parts).toEqual([ - { type: "text", text: "Image file: diagram.png" }, - expect.objectContaining({ type: "inline-data", mimeType: "image/png" }), - ]); - expect(built?.structuredInputBytes).toBeGreaterThan(0); - }); - - it("skips lazy multimodal indexing when file state changes after discovery", async () => { - for (const testCase of [ - { - name: "grows", - mutate: (target: string, entrySize: number) => { - fsSync.writeFileSync(target, Buffer.alloc(entrySize + 32, 1)); - }, - }, - { - name: "bytes change", - mutate: (target: string) => { - fsSync.writeFileSync(target, Buffer.from("gif")); - }, - }, - { - name: "disappears", - mutate: (target: string) => { - fsSync.rmSync(target); - }, - }, - ] as const) { - const tmpDir = getTmpDir(); - const target = path.join(tmpDir, `${testCase.name}.png`); - fsSync.writeFileSync(target, Buffer.from("png")); - - const entry = await buildFileEntry(target, tmpDir, multimodal); - expect(entry, testCase.name).not.toBeNull(); - testCase.mutate(target, entry!.size); - - await expect(buildMultimodalChunkForIndexing(entry!), testCase.name).resolves.toBeNull(); - } - }); -}); - -describe("chunkMarkdown", () => { - it("splits overly long lines into max-sized chunks", () => { - const chunkTokens = 400; - const maxChars = chunkTokens * 4; - const content = "a".repeat(maxChars * 3 + 25); - const chunks = chunkMarkdown(content, { tokens: chunkTokens, overlap: 0 }); - expect(chunks.length).toBeGreaterThan(1); - for (const chunk of chunks) { - expect(chunk.text.length).toBeLessThanOrEqual(maxChars); - } - }); - - it("produces more chunks for CJK text than for equal-length ASCII text", () => { - // CJK chars ≈ 1 token each; ASCII chars ≈ 0.25 tokens each. - // For the same raw character count, CJK content should produce more chunks - // because each character "weighs" ~4× more in token estimation. - const chunkTokens = 50; - - // 400 ASCII chars → ~100 tokens → fits in ~2 chunks - const asciiLines = Array.from({ length: 20 }, () => "a".repeat(20)).join("\n"); - const asciiChunks = chunkMarkdown(asciiLines, { tokens: chunkTokens, overlap: 0 }); - - // 400 CJK chars → ~400 tokens → needs ~8 chunks - const cjkLines = Array.from({ length: 20 }, () => "你".repeat(20)).join("\n"); - const cjkChunks = chunkMarkdown(cjkLines, { tokens: chunkTokens, overlap: 0 }); - - expect(cjkChunks.length).toBeGreaterThan(asciiChunks.length); - }); - - it("respects token budget for Chinese text", () => { - // With tokens=100, each CJK char ≈ 1 token, so chunks should hold ~100 CJK chars. - const chunkTokens = 100; - const lines: string[] = []; - for (let i = 0; i < 50; i++) { - lines.push("这是一个测试句子用来验证分块逻辑是否正确处理中文文本内容"); - } - const content = lines.join("\n"); - const chunks = chunkMarkdown(content, { tokens: chunkTokens, overlap: 0 }); - - expect(chunks.length).toBeGreaterThan(1); - // Each chunk's CJK content should not vastly exceed the token budget. - // With CJK-aware estimation, each char ≈ 1 token, so chunk text length - // (in CJK chars) should be roughly <= tokens budget (with some tolerance - // for line boundaries). - for (const chunk of chunks) { - // Count actual CJK characters in the chunk - const cjkCount = (chunk.text.match(/[\u4e00-\u9fff]/g) ?? []).length; - // Allow 2× tolerance for line-boundary rounding - expect(cjkCount).toBeLessThanOrEqual(chunkTokens * 2); - } - }); - - it("keeps English chunking behavior unchanged", () => { - const chunkTokens = 100; - const maxChars = chunkTokens * 4; // 400 chars - const content = "hello world this is a test. ".repeat(50); - const chunks = chunkMarkdown(content, { tokens: chunkTokens, overlap: 0 }); - expect(chunks.length).toBeGreaterThan(1); - for (const chunk of chunks) { - expect(chunk.text.length).toBeLessThanOrEqual(maxChars); - } - }); - - it("handles mixed CJK and ASCII content correctly", () => { - const chunkTokens = 50; - const lines: string[] = []; - for (let i = 0; i < 30; i++) { - lines.push(`Line ${i}: 这是中英文混合的测试内容 with some English text`); - } - const content = lines.join("\n"); - const chunks = chunkMarkdown(content, { tokens: chunkTokens, overlap: 0 }); - // Should produce multiple chunks and not crash - expect(chunks.length).toBeGreaterThan(1); - // Verify all content is preserved - const reconstructed = chunks.map((c) => c.text).join("\n"); - // Due to overlap=0, the concatenated chunks should cover all lines - expect(reconstructed).toContain("Line 0"); - expect(reconstructed).toContain("Line 29"); - }); - - it("splits very long CJK lines into budget-sized segments", () => { - // A single line of 2000 CJK characters (no newlines). - // With tokens=200, each CJK char ≈ 1 token. - const longCjkLine = "中".repeat(2000); - const chunks = chunkMarkdown(longCjkLine, { tokens: 200, overlap: 0 }); - expect(chunks.length).toBeGreaterThanOrEqual(8); - for (const chunk of chunks) { - const cjkCount = (chunk.text.match(/[\u4E00-\u9FFF]/g) ?? []).length; - expect(cjkCount).toBeLessThanOrEqual(200 * 2); - } - }); - it("does not break surrogate pairs when splitting long CJK lines", () => { - // "𠀀" (U+20000) is a surrogate pair: 2 UTF-16 code units per character. - // With an odd token budget, the fine-split must not cut inside a pair. - const surrogateChar = "\u{20000}"; // 𠀀 - const longLine = surrogateChar.repeat(120); - const chunks = chunkMarkdown(longLine, { tokens: 31, overlap: 0 }); - for (const chunk of chunks) { - // No chunk should contain the Unicode replacement character U+FFFD, - // which would indicate a broken surrogate pair. - expect(chunk.text).not.toContain("\uFFFD"); - // Every character in the chunk should be a valid string (no lone surrogates). - for (let i = 0; i < chunk.text.length; i += 1) { - const code = chunk.text.charCodeAt(i); - if (code >= 0xd800 && code <= 0xdbff) { - // High surrogate must be followed by a low surrogate - const next = chunk.text.charCodeAt(i + 1); - expect(next).toBeGreaterThanOrEqual(0xdc00); - expect(next).toBeLessThanOrEqual(0xdfff); - } - } - } - }); - it("does not over-split long Latin lines (backward compat)", () => { - // 2000 ASCII chars / 800 maxChars -> about 3 segments, not 10 tiny ones. - const longLatinLine = "a".repeat(2000); - const chunks = chunkMarkdown(longLatinLine, { tokens: 200, overlap: 0 }); - expect(chunks.length).toBeLessThanOrEqual(5); - }); -}); - -describe("remapChunkLines", () => { - it("remaps chunk line numbers using a lineMap", () => { - // Simulate 5 content lines that came from JSONL lines [4, 6, 7, 10, 13] (1-indexed) - const lineMap = [4, 6, 7, 10, 13]; - - // Create chunks from content that has 5 lines - const content = "User: Hello\nAssistant: Hi\nUser: Question\nAssistant: Answer\nUser: Thanks"; - const chunks = chunkMarkdown(content, { tokens: 400, overlap: 0 }); - expect(chunks.length).toBeGreaterThan(0); - - // Before remapping, startLine/endLine reference content line numbers (1-indexed) - expect(chunks[0].startLine).toBe(1); - - // Remap - remapChunkLines(chunks, lineMap); - - // After remapping, line numbers should reference original JSONL lines - // Content line 1 → JSONL line 4, content line 5 → JSONL line 13 - expect(chunks[0].startLine).toBe(4); - const lastChunk = chunks[chunks.length - 1]; - expect(lastChunk.endLine).toBe(13); - }); - - it("preserves original line numbers when lineMap is undefined", () => { - const content = "Line one\nLine two\nLine three"; - const chunks = chunkMarkdown(content, { tokens: 400, overlap: 0 }); - const originalStart = chunks[0].startLine; - const originalEnd = chunks[chunks.length - 1].endLine; - - remapChunkLines(chunks, undefined); - - expect(chunks[0].startLine).toBe(originalStart); - expect(chunks[chunks.length - 1].endLine).toBe(originalEnd); - }); - - it("handles multi-chunk content with correct remapping", () => { - // Use small chunk size to force multiple chunks - // lineMap: 10 content lines from JSONL lines [2, 5, 8, 11, 14, 17, 20, 23, 26, 29] - const lineMap = [2, 5, 8, 11, 14, 17, 20, 23, 26, 29]; - const contentLines = lineMap.map((_, i) => - i % 2 === 0 ? `User: Message ${i}` : `Assistant: Reply ${i}`, - ); - const content = contentLines.join("\n"); - - // Use very small chunk size to force splitting - const chunks = chunkMarkdown(content, { tokens: 10, overlap: 0 }); - expect(chunks.length).toBeGreaterThan(1); - - remapChunkLines(chunks, lineMap); - - // First chunk should start at JSONL line 2 - expect(chunks[0].startLine).toBe(2); - // Last chunk should end at JSONL line 29 - expect(chunks[chunks.length - 1].endLine).toBe(29); - - // Each chunk's startLine should be ≤ its endLine - for (const chunk of chunks) { - expect(chunk.startLine).toBeLessThanOrEqual(chunk.endLine); - } - }); -}); diff --git a/src/memory-host-sdk/host/internal.ts b/src/memory-host-sdk/host/internal.ts deleted file mode 100644 index 12d27c15f3e..00000000000 --- a/src/memory-host-sdk/host/internal.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "../../../packages/memory-host-sdk/src/host/internal.js"; diff --git a/src/memory-host-sdk/host/memory-schema.ts b/src/memory-host-sdk/host/memory-schema.ts deleted file mode 100644 index 5264d758218..00000000000 --- a/src/memory-host-sdk/host/memory-schema.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "../../../packages/memory-host-sdk/src/host/memory-schema.js"; diff --git a/src/memory-host-sdk/host/mirror.test.ts b/src/memory-host-sdk/host/mirror.test.ts deleted file mode 100644 index 4b1dc385c56..00000000000 --- a/src/memory-host-sdk/host/mirror.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { describe, expect, it } from "vitest"; - -const HOST_DIR = path.dirname(fileURLToPath(import.meta.url)); - -const PACKAGE_BRIDGE_FILES = [ - "backend-config.ts", - "batch-error-utils.ts", - "batch-output.ts", - "batch-status.ts", - "embedding-input-limits.ts", - "embeddings-remote-provider.ts", - "embeddings.ts", - "internal.ts", - "memory-schema.ts", - "multimodal.ts", - "qmd-process.ts", - "qmd-scope.ts", - "query-expansion.ts", - "read-file-shared.ts", - "read-file.ts", - "session-files.ts", - "types.ts", -] as const; - -describe("memory-host-sdk host package bridges", () => { - it("keeps package-owned source bridges thin", () => { - for (const fileName of PACKAGE_BRIDGE_FILES) { - const source = fs.readFileSync(path.join(HOST_DIR, fileName), "utf8"); - expect(source, fileName).toBe( - `export * from "../../../packages/memory-host-sdk/src/host/${fileName.replace( - /\.ts$/u, - ".js", - )}";\n`, - ); - } - }); -}); diff --git a/src/memory-host-sdk/host/multimodal.ts b/src/memory-host-sdk/host/multimodal.ts deleted file mode 100644 index 200f20a715b..00000000000 --- a/src/memory-host-sdk/host/multimodal.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "../../../packages/memory-host-sdk/src/host/multimodal.js"; diff --git a/src/memory-host-sdk/host/node-llama.ts b/src/memory-host-sdk/host/node-llama.ts deleted file mode 100644 index 8871b65da2e..00000000000 --- a/src/memory-host-sdk/host/node-llama.ts +++ /dev/null @@ -1,31 +0,0 @@ -export type LlamaEmbedding = { - vector: Float32Array | number[]; -}; - -export type LlamaEmbeddingContext = { - getEmbeddingFor: (text: string) => Promise; -}; - -export type LlamaModel = { - createEmbeddingContext: (options?: { - contextSize?: number | "auto"; - }) => Promise; -}; - -export type Llama = { - loadModel: (params: { modelPath: string }) => Promise; -}; - -export type NodeLlamaCppModule = { - LlamaLogLevel: { - error: number; - }; - getLlama: (params: { logLevel: number }) => Promise; - resolveModelFile: (modelPath: string, cacheDir?: string) => Promise; -}; - -const NODE_LLAMA_CPP_MODULE = "node-llama-cpp"; - -export async function importNodeLlamaCpp() { - return import(NODE_LLAMA_CPP_MODULE) as Promise; -} diff --git a/src/memory-host-sdk/host/post-json.test.ts b/src/memory-host-sdk/host/post-json.test.ts deleted file mode 100644 index a29b2389908..00000000000 --- a/src/memory-host-sdk/host/post-json.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import { postJson } from "./post-json.js"; - -describe("postJson", () => { - it("parses JSON from an injected fetch response", async () => { - const fetchImpl = vi.fn( - async () => - new Response(JSON.stringify({ ok: true }), { - status: 200, - headers: { "content-type": "application/json" }, - }), - ) as typeof fetch; - - const result = await postJson({ - url: "https://example.com/v1/post", - headers: { Authorization: "Bearer test" }, - ssrfPolicy: { allowedHostnames: ["example.com"] }, - fetchImpl, - body: { input: ["x"] }, - errorPrefix: "post failed", - parse: (payload) => payload, - }); - - expect(result).toEqual({ ok: true }); - }); -}); diff --git a/src/memory-host-sdk/host/post-json.ts b/src/memory-host-sdk/host/post-json.ts deleted file mode 100644 index 80ae54320f1..00000000000 --- a/src/memory-host-sdk/host/post-json.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { SsrFPolicy } from "../../infra/net/ssrf.js"; -import { withRemoteHttpResponse } from "./remote-http.js"; - -export async function postJson(params: { - url: string; - headers: Record; - ssrfPolicy?: SsrFPolicy; - fetchImpl?: typeof fetch; - body: unknown; - errorPrefix: string; - attachStatus?: boolean; - parse: (payload: unknown) => T | Promise; -}): Promise { - return await withRemoteHttpResponse({ - url: params.url, - ssrfPolicy: params.ssrfPolicy, - fetchImpl: params.fetchImpl, - init: { - method: "POST", - headers: params.headers, - body: JSON.stringify(params.body), - }, - onResponse: async (res) => { - if (!res.ok) { - const text = await res.text(); - const err = new Error(`${params.errorPrefix}: ${res.status} ${text}`) as Error & { - status?: number; - }; - if (params.attachStatus) { - err.status = res.status; - } - throw err; - } - return await params.parse(await res.json()); - }, - }); -} diff --git a/src/memory-host-sdk/host/qmd-process.ts b/src/memory-host-sdk/host/qmd-process.ts deleted file mode 100644 index 127233da6b9..00000000000 --- a/src/memory-host-sdk/host/qmd-process.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "../../../packages/memory-host-sdk/src/host/qmd-process.js"; diff --git a/src/memory-host-sdk/host/qmd-query-parser.test.ts b/src/memory-host-sdk/host/qmd-query-parser.test.ts deleted file mode 100644 index e16ffa9150e..00000000000 --- a/src/memory-host-sdk/host/qmd-query-parser.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { parseQmdQueryJson } from "./qmd-query-parser.js"; - -describe("parseQmdQueryJson", () => { - it("parses clean qmd JSON output", () => { - const results = parseQmdQueryJson('[{"docid":"abc","score":1,"snippet":"@@ -1,1\\none"}]', ""); - expect(results).toEqual([ - { - docid: "abc", - score: 1, - snippet: "@@ -1,1\none", - }, - ]); - }); - - it("extracts embedded result arrays from noisy stdout", () => { - const results = parseQmdQueryJson( - `initializing -{"payload":"ok"} -[{"docid":"abc","score":0.5}] -complete`, - "", - ); - expect(results).toEqual([{ docid: "abc", score: 0.5 }]); - }); - - it("preserves explicit qmd line metadata when present", () => { - const results = parseQmdQueryJson( - '[{"docid":"abc","score":0.5,"start_line":4,"end_line":6,"snippet":"@@ -10,1\\nignored"}]', - "", - ); - expect(results).toEqual([ - { - docid: "abc", - score: 0.5, - snippet: "@@ -10,1\nignored", - startLine: 4, - endLine: 6, - }, - ]); - }); - - it("maps single-line qmd line metadata onto both line bounds", () => { - const results = parseQmdQueryJson('[{"docid":"abc","score":0.5,"line":9}]', ""); - expect(results).toEqual([ - { - docid: "abc", - score: 0.5, - startLine: 9, - endLine: 9, - }, - ]); - }); - - it("treats plain-text no-results from stderr as an empty result set", () => { - const results = parseQmdQueryJson("", "No results found\n"); - expect(results).toEqual([]); - }); - - it("treats prefixed no-results marker output as an empty result set", () => { - expect(parseQmdQueryJson("warning: no results found", "")).toEqual([]); - expect(parseQmdQueryJson("", "[qmd] warning: no results found\n")).toEqual([]); - }); - - it("does not treat arbitrary non-marker text as no-results output", () => { - expect(() => - parseQmdQueryJson("warning: search completed; no results found for this query", ""), - ).toThrow(/qmd query returned invalid JSON/i); - }); - - it("throws when stdout cannot be interpreted as qmd JSON", () => { - expect(() => parseQmdQueryJson("this is not json", "")).toThrow( - /qmd query returned invalid JSON/i, - ); - }); -}); diff --git a/src/memory-host-sdk/host/qmd-query-parser.ts b/src/memory-host-sdk/host/qmd-query-parser.ts deleted file mode 100644 index a6ec8bf049f..00000000000 --- a/src/memory-host-sdk/host/qmd-query-parser.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { formatErrorMessage } from "../../infra/errors.js"; -import { createSubsystemLogger } from "../../logging/subsystem.js"; -import { extractBalancedJsonPrefix } from "../../shared/balanced-json.js"; -import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; - -const log = createSubsystemLogger("memory"); - -export type QmdQueryResult = { - docid?: string; - score?: number; - collection?: string; - file?: string; - snippet?: string; - body?: string; - startLine?: number; - endLine?: number; -}; - -export function parseQmdQueryJson(stdout: string, stderr: string): QmdQueryResult[] { - const trimmedStdout = stdout.trim(); - const trimmedStderr = stderr.trim(); - const stdoutIsMarker = trimmedStdout.length > 0 && isQmdNoResultsOutput(trimmedStdout); - const stderrIsMarker = trimmedStderr.length > 0 && isQmdNoResultsOutput(trimmedStderr); - if (stdoutIsMarker || (!trimmedStdout && stderrIsMarker)) { - return []; - } - if (!trimmedStdout) { - const context = trimmedStderr ? ` (stderr: ${summarizeQmdStderr(trimmedStderr)})` : ""; - const message = `stdout empty${context}`; - log.warn(`qmd query returned invalid JSON: ${message}`); - throw new Error(`qmd query returned invalid JSON: ${message}`); - } - try { - const parsed = parseQmdQueryResultArray(trimmedStdout); - if (parsed !== null) { - return parsed; - } - const noisyPayload = extractBalancedJsonPrefix(trimmedStdout, { openers: ["["] })?.json; - if (!noisyPayload) { - throw new Error("qmd query JSON response was not an array"); - } - const fallback = parseQmdQueryResultArray(noisyPayload); - if (fallback !== null) { - return fallback; - } - throw new Error("qmd query JSON response was not an array"); - } catch (err) { - const message = formatErrorMessage(err); - log.warn(`qmd query returned invalid JSON: ${message}`); - throw new Error(`qmd query returned invalid JSON: ${message}`, { cause: err }); - } -} - -function isQmdNoResultsOutput(raw: string): boolean { - const lines = raw - .split(/\r?\n/) - .map((line) => normalizeLowercaseStringOrEmpty(line).replace(/\s+/g, " ")) - .filter((line) => line.length > 0); - return lines.some((line) => isQmdNoResultsLine(line)); -} - -function isQmdNoResultsLine(line: string): boolean { - if (line === "no results found" || line === "no results found.") { - return true; - } - return /^(?:\[[^\]]+\]\s*)?(?:(?:warn(?:ing)?|info|error|qmd)\s*:\s*)+no results found\.?$/.test( - line, - ); -} - -function summarizeQmdStderr(raw: string): string { - return raw.length <= 120 ? raw : `${raw.slice(0, 117)}...`; -} - -function parseQmdQueryResultArray(raw: string): QmdQueryResult[] | null { - try { - const parsed = JSON.parse(raw) as unknown; - if (!Array.isArray(parsed)) { - return null; - } - return parsed.map((item) => { - if (typeof item !== "object" || item === null) { - return item as QmdQueryResult; - } - const record = item as Record; - const docid = typeof record.docid === "string" ? record.docid : undefined; - const score = - typeof record.score === "number" && Number.isFinite(record.score) - ? record.score - : undefined; - const collection = typeof record.collection === "string" ? record.collection : undefined; - const file = typeof record.file === "string" ? record.file : undefined; - const snippet = typeof record.snippet === "string" ? record.snippet : undefined; - const body = typeof record.body === "string" ? record.body : undefined; - const line = parseQmdLineNumber(record.line); - const startLine = parseQmdLineNumber(record.start_line ?? record.startLine) ?? line; - const endLine = parseQmdLineNumber(record.end_line ?? record.endLine) ?? line; - return { - docid, - score, - collection, - file, - snippet, - body, - startLine, - endLine, - } as QmdQueryResult; - }); - } catch { - return null; - } -} - -function parseQmdLineNumber(value: unknown): number | undefined { - return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined; -} diff --git a/src/memory-host-sdk/host/qmd-scope.test.ts b/src/memory-host-sdk/host/qmd-scope.test.ts deleted file mode 100644 index 5a826e9c9b3..00000000000 --- a/src/memory-host-sdk/host/qmd-scope.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { describe, expect, it } from "vitest"; -import type { ResolvedQmdConfig } from "./backend-config.js"; -import { deriveQmdScopeChannel, deriveQmdScopeChatType, isQmdScopeAllowed } from "./qmd-scope.js"; - -describe("qmd scope", () => { - const allowDirect: ResolvedQmdConfig["scope"] = { - default: "deny", - rules: [{ action: "allow", match: { chatType: "direct" } }], - }; - - it("derives channel and chat type from canonical keys once", () => { - expect(deriveQmdScopeChannel("Workspace:group:123")).toBe("workspace"); - expect(deriveQmdScopeChatType("Workspace:group:123")).toBe("group"); - }); - - it("derives channel and chat type from stored key suffixes", () => { - expect(deriveQmdScopeChannel("agent:agent-1:workspace:channel:chan-123")).toBe("workspace"); - expect(deriveQmdScopeChatType("agent:agent-1:workspace:channel:chan-123")).toBe("channel"); - }); - - it("treats parsed keys with no chat prefix as direct", () => { - expect(deriveQmdScopeChannel("agent:agent-1:peer-direct")).toBeUndefined(); - expect(deriveQmdScopeChatType("agent:agent-1:peer-direct")).toBe("direct"); - expect(isQmdScopeAllowed(allowDirect, "agent:agent-1:peer-direct")).toBe(true); - expect(isQmdScopeAllowed(allowDirect, "agent:agent-1:peer:group:abc")).toBe(false); - }); - - it("applies scoped key-prefix checks against normalized key", () => { - const scope: ResolvedQmdConfig["scope"] = { - default: "deny", - rules: [{ action: "allow", match: { keyPrefix: "workspace:" } }], - }; - expect(isQmdScopeAllowed(scope, "agent:agent-1:workspace:group:123")).toBe(true); - expect(isQmdScopeAllowed(scope, "agent:agent-1:other:group:123")).toBe(false); - }); - - it("supports rawKeyPrefix matches for agent-prefixed keys", () => { - const scope: ResolvedQmdConfig["scope"] = { - default: "allow", - rules: [{ action: "deny", match: { rawKeyPrefix: "agent:main:discord:" } }], - }; - expect(isQmdScopeAllowed(scope, "agent:main:discord:channel:c123")).toBe(false); - expect(isQmdScopeAllowed(scope, "agent:main:slack:channel:c123")).toBe(true); - }); - - it("keeps legacy agent-prefixed keyPrefix rules working", () => { - const scope: ResolvedQmdConfig["scope"] = { - default: "allow", - rules: [{ action: "deny", match: { keyPrefix: "agent:main:discord:" } }], - }; - expect(isQmdScopeAllowed(scope, "agent:main:discord:channel:c123")).toBe(false); - expect(isQmdScopeAllowed(scope, "agent:main:slack:channel:c123")).toBe(true); - }); -}); diff --git a/src/memory-host-sdk/host/qmd-scope.ts b/src/memory-host-sdk/host/qmd-scope.ts deleted file mode 100644 index 22ed5005a14..00000000000 --- a/src/memory-host-sdk/host/qmd-scope.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "../../../packages/memory-host-sdk/src/host/qmd-scope.js"; diff --git a/src/memory-host-sdk/host/query-expansion.ts b/src/memory-host-sdk/host/query-expansion.ts deleted file mode 100644 index 2c941262724..00000000000 --- a/src/memory-host-sdk/host/query-expansion.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "../../../packages/memory-host-sdk/src/host/query-expansion.js"; diff --git a/src/memory-host-sdk/host/read-file-shared.ts b/src/memory-host-sdk/host/read-file-shared.ts deleted file mode 100644 index 08d16a8e3d4..00000000000 --- a/src/memory-host-sdk/host/read-file-shared.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "../../../packages/memory-host-sdk/src/host/read-file-shared.js"; diff --git a/src/memory-host-sdk/host/read-file.ts b/src/memory-host-sdk/host/read-file.ts deleted file mode 100644 index af586356207..00000000000 --- a/src/memory-host-sdk/host/read-file.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "../../../packages/memory-host-sdk/src/host/read-file.js"; diff --git a/src/memory-host-sdk/host/remote-http.test.ts b/src/memory-host-sdk/host/remote-http.test.ts deleted file mode 100644 index 675e74c3fe2..00000000000 --- a/src/memory-host-sdk/host/remote-http.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { GUARDED_FETCH_MODE } from "../../infra/net/fetch-guard.js"; -import { withRemoteHttpResponse } from "./remote-http.js"; - -describe("withRemoteHttpResponse", () => { - function makeFetchDeps({ useEnvProxy = false }: { useEnvProxy?: boolean } = {}) { - const calls: unknown[] = []; - return { - calls, - fetchWithSsrFGuardImpl: async (params: unknown) => { - calls.push(params); - return { - response: new Response("ok", { status: 200 }), - finalUrl: "https://memory.example/v1", - release: async () => {}, - }; - }, - shouldUseEnvHttpProxyForUrlImpl: () => useEnvProxy, - }; - } - - it("uses trusted env proxy mode when the target will use EnvHttpProxyAgent", async () => { - const deps = makeFetchDeps({ useEnvProxy: true }); - - await withRemoteHttpResponse({ - url: "https://memory.example/v1/embeddings", - onResponse: async () => undefined, - ...deps, - }); - - expect(deps.calls[0]).toEqual( - expect.objectContaining({ - url: "https://memory.example/v1/embeddings", - mode: GUARDED_FETCH_MODE.TRUSTED_ENV_PROXY, - }), - ); - }); - - it("keeps strict guarded fetch mode when proxy env would not proxy the target", async () => { - const deps = makeFetchDeps(); - - await withRemoteHttpResponse({ - url: "https://internal.corp.example/v1/embeddings", - onResponse: async () => undefined, - ...deps, - }); - - expect(deps.calls[0]).toBeDefined(); - expect(deps.calls[0]).not.toHaveProperty("mode"); - }); -}); diff --git a/src/memory-host-sdk/host/remote-http.ts b/src/memory-host-sdk/host/remote-http.ts deleted file mode 100644 index 187af8afa6d..00000000000 --- a/src/memory-host-sdk/host/remote-http.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { fetchWithSsrFGuard, GUARDED_FETCH_MODE } from "../../infra/net/fetch-guard.js"; -import { shouldUseEnvHttpProxyForUrl } from "../../infra/net/proxy-env.js"; -import { ssrfPolicyFromHttpBaseUrlAllowedHostname, type SsrFPolicy } from "../../infra/net/ssrf.js"; - -export const buildRemoteBaseUrlPolicy = ssrfPolicyFromHttpBaseUrlAllowedHostname; - -export async function withRemoteHttpResponse(params: { - url: string; - init?: RequestInit; - ssrfPolicy?: SsrFPolicy; - fetchImpl?: typeof fetch; - fetchWithSsrFGuardImpl?: typeof fetchWithSsrFGuard; - shouldUseEnvHttpProxyForUrlImpl?: typeof shouldUseEnvHttpProxyForUrl; - auditContext?: string; - onResponse: (response: Response) => Promise; -}): Promise { - const guardedFetch = params.fetchWithSsrFGuardImpl ?? fetchWithSsrFGuard; - const shouldUseEnvProxy = params.shouldUseEnvHttpProxyForUrlImpl ?? shouldUseEnvHttpProxyForUrl; - const { response, release } = await guardedFetch({ - url: params.url, - fetchImpl: params.fetchImpl, - init: params.init, - policy: params.ssrfPolicy, - auditContext: params.auditContext ?? "memory-remote", - ...(shouldUseEnvProxy(params.url) ? { mode: GUARDED_FETCH_MODE.TRUSTED_ENV_PROXY } : {}), - }); - try { - return await params.onResponse(response); - } finally { - await release(); - } -} diff --git a/src/memory-host-sdk/host/secret-input.test.ts b/src/memory-host-sdk/host/secret-input.test.ts deleted file mode 100644 index 18c9a8e8d59..00000000000 --- a/src/memory-host-sdk/host/secret-input.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { resolveMemorySecretInputString } from "./secret-input.js"; - -describe("resolveMemorySecretInputString", () => { - const googleApiKeyRef = { - source: "env", - provider: "default", - id: "GOOGLE_API_KEY", - }; - - it("uses the daemon env for env-backed SecretRefs", () => { - expect( - resolveMemorySecretInputString({ - value: googleApiKeyRef, - path: "agents.main.memorySearch.remote.apiKey", - env: { GOOGLE_API_KEY: "resolved-key" }, - }), - ).toBe("resolved-key"); - }); - - it("still throws when an env-backed SecretRef is missing from the daemon env", () => { - expect(() => - resolveMemorySecretInputString({ - value: googleApiKeyRef, - path: "agents.main.memorySearch.remote.apiKey", - env: {}, - }), - ).toThrow(/unresolved SecretRef/); - }); -}); diff --git a/src/memory-host-sdk/host/secret-input.ts b/src/memory-host-sdk/host/secret-input.ts deleted file mode 100644 index 6b3f3a89595..00000000000 --- a/src/memory-host-sdk/host/secret-input.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { - hasConfiguredSecretInput, - normalizeResolvedSecretInputString, - normalizeSecretInputString, - resolveSecretInputRef, -} from "../../config/types.secrets.js"; - -export function hasConfiguredMemorySecretInput(value: unknown): boolean { - return hasConfiguredSecretInput(value); -} - -export function resolveMemorySecretInputString(params: { - value: unknown; - path: string; - env?: NodeJS.ProcessEnv; -}): string | undefined { - const { ref } = resolveSecretInputRef({ value: params.value }); - if (ref?.source === "env") { - const envValue = normalizeSecretInputString((params.env ?? process.env)[ref.id]); - if (envValue) { - return envValue; - } - } - return normalizeResolvedSecretInputString({ - value: params.value, - path: params.path, - }); -} diff --git a/src/memory-host-sdk/host/session-files.test.ts b/src/memory-host-sdk/host/session-files.test.ts deleted file mode 100644 index 6a369b376fa..00000000000 --- a/src/memory-host-sdk/host/session-files.test.ts +++ /dev/null @@ -1,710 +0,0 @@ -import fsSync from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -import { buildSessionEntry, listSessionFilesForAgent } from "./session-files.js"; - -let fixtureRoot: string; -let tmpDir: string; -let originalStateDir: string | undefined; -let fixtureId = 0; - -beforeAll(() => { - fixtureRoot = fsSync.mkdtempSync(path.join(os.tmpdir(), "session-entry-test-")); -}); - -afterAll(() => { - fsSync.rmSync(fixtureRoot, { recursive: true, force: true }); -}); - -beforeEach(() => { - tmpDir = path.join(fixtureRoot, `case-${fixtureId++}`); - fsSync.mkdirSync(tmpDir, { recursive: true }); - originalStateDir = process.env.OPENCLAW_STATE_DIR; - process.env.OPENCLAW_STATE_DIR = tmpDir; -}); - -afterEach(() => { - if (originalStateDir === undefined) { - delete process.env.OPENCLAW_STATE_DIR; - } else { - process.env.OPENCLAW_STATE_DIR = originalStateDir; - } -}); - -function expectNoUnpairedSurrogates(value: string): void { - for (let index = 0; index < value.length; index += 1) { - const code = value.charCodeAt(index); - if (code >= 0xd800 && code <= 0xdbff) { - expect(index + 1).toBeLessThan(value.length); - const next = value.charCodeAt(index + 1); - expect(next).toBeGreaterThanOrEqual(0xdc00); - expect(next).toBeLessThanOrEqual(0xdfff); - index += 1; - continue; - } - expect(code < 0xdc00 || code > 0xdfff).toBe(true); - } -} - -function writeSessionJsonl(fileName: string, records: readonly unknown[]): string { - const filePath = path.join(tmpDir, fileName); - fsSync.writeFileSync(filePath, records.map((record) => JSON.stringify(record)).join("\n")); - return filePath; -} - -function buildSessionEntryWithoutStoreClassification(filePath: string) { - return buildSessionEntry(filePath, { - generatedByCronRun: false, - generatedByDreamingNarrative: false, - }); -} - -describe("listSessionFilesForAgent", () => { - it("includes reset and deleted transcripts in session file listing", async () => { - const sessionsDir = path.join(tmpDir, "agents", "main", "sessions"); - fsSync.mkdirSync(path.join(sessionsDir, "archive"), { recursive: true }); - - const included = [ - "active.jsonl", - "active.jsonl.reset.2026-02-16T22-26-33.000Z", - "active.jsonl.deleted.2026-02-16T22-27-33.000Z", - ]; - const excluded = ["active.jsonl.bak.2026-02-16T22-28-33.000Z", "sessions.json", "notes.md"]; - - for (const fileName of [...included, ...excluded]) { - fsSync.writeFileSync(path.join(sessionsDir, fileName), ""); - } - fsSync.writeFileSync( - path.join(sessionsDir, "archive", "nested.jsonl.deleted.2026-02-16T22-29-33.000Z"), - "", - ); - - const files = await listSessionFilesForAgent("main"); - - expect(files.map((filePath) => path.basename(filePath)).toSorted()).toEqual( - included.toSorted(), - ); - }); -}); - -describe("buildSessionEntry", () => { - it("returns lineMap tracking original JSONL line numbers", async () => { - // Simulate a real session JSONL file with metadata records interspersed - // Lines 1-3: non-message metadata records - // Line 4: user message - // Line 5: metadata - // Line 6: assistant message - // Line 7: user message - const jsonlLines = [ - JSON.stringify({ type: "custom", customType: "model-snapshot", data: {} }), - JSON.stringify({ type: "custom", customType: "openclaw.cache-ttl", data: {} }), - JSON.stringify({ type: "session-meta", agentId: "test" }), - JSON.stringify({ type: "message", message: { role: "user", content: "Hello world" } }), - JSON.stringify({ type: "custom", customType: "tool-result", data: {} }), - JSON.stringify({ - type: "message", - message: { role: "assistant", content: "Hi there, how can I help?" }, - }), - JSON.stringify({ type: "message", message: { role: "user", content: "Tell me a joke" } }), - ]; - const filePath = path.join(tmpDir, "session.jsonl"); - fsSync.writeFileSync(filePath, jsonlLines.join("\n")); - - const entry = await buildSessionEntryWithoutStoreClassification(filePath); - expect(entry).not.toBeNull(); - - // The content should have 3 lines (3 message records) - const contentLines = entry!.content.split("\n"); - expect(contentLines).toHaveLength(3); - expect(contentLines[0]).toContain("User: Hello world"); - expect(contentLines[1]).toContain("Assistant: Hi there"); - expect(contentLines[2]).toContain("User: Tell me a joke"); - - // lineMap should map each content line to its original JSONL line (1-indexed) - // Content line 0 → JSONL line 4 (the first user message) - // Content line 1 → JSONL line 6 (the assistant message) - // Content line 2 → JSONL line 7 (the second user message) - expect(entry!.lineMap).toBeDefined(); - expect(entry!.lineMap).toEqual([4, 6, 7]); - expect(entry!.messageTimestampsMs).toEqual([0, 0, 0]); - }); - - it("returns empty lineMap when no messages are found", async () => { - const jsonlLines = [ - JSON.stringify({ type: "custom", customType: "model-snapshot", data: {} }), - JSON.stringify({ type: "session-meta", agentId: "test" }), - ]; - const filePath = path.join(tmpDir, "empty-session.jsonl"); - fsSync.writeFileSync(filePath, jsonlLines.join("\n")); - - const entry = await buildSessionEntryWithoutStoreClassification(filePath); - expect(entry).not.toBeNull(); - expect(entry!.content).toBe(""); - expect(entry!.lineMap).toEqual([]); - expect(entry!.messageTimestampsMs).toEqual([]); - }); - - it("skips blank lines and invalid JSON without breaking lineMap", async () => { - const jsonlLines = [ - "", - "not valid json", - JSON.stringify({ type: "message", message: { role: "user", content: "First" } }), - "", - JSON.stringify({ type: "message", message: { role: "assistant", content: "Second" } }), - ]; - const filePath = path.join(tmpDir, "gaps.jsonl"); - fsSync.writeFileSync(filePath, jsonlLines.join("\n")); - - const entry = await buildSessionEntryWithoutStoreClassification(filePath); - expect(entry).not.toBeNull(); - expect(entry!.lineMap).toEqual([3, 5]); - expect(entry!.messageTimestampsMs).toEqual([0, 0]); - }); - - it("captures message timestamps when present", async () => { - const jsonlLines = [ - JSON.stringify({ - type: "message", - timestamp: "2026-04-05T10:00:00.000Z", - message: { role: "user", content: "First" }, - }), - JSON.stringify({ - type: "message", - message: { - role: "assistant", - timestamp: "2026-04-05T10:01:00.000Z", - content: "Second", - }, - }), - ]; - const filePath = path.join(tmpDir, "timestamps.jsonl"); - fsSync.writeFileSync(filePath, jsonlLines.join("\n")); - - const entry = await buildSessionEntryWithoutStoreClassification(filePath); - expect(entry).not.toBeNull(); - expect(entry!.messageTimestampsMs).toEqual([ - Date.parse("2026-04-05T10:00:00.000Z"), - Date.parse("2026-04-05T10:01:00.000Z"), - ]); - }); - - it("strips inbound metadata envelope from user messages before normalization", async () => { - // Representative inbound envelope: Conversation info + Sender blocks prepended - // to the actual user text. Without stripping, the JSON envelope dominates - // the corpus entry and the user's real words get truncated by the - // SESSION_INGESTION_MAX_SNIPPET_CHARS cap downstream. - // See: https://github.com/openclaw/openclaw/issues/63921 - const envelopedUserText = [ - "Conversation info (untrusted metadata):", - "```json", - '{"message_id":"msg-100","chat_id":"-100123","sender":"Chris"}', - "```", - "", - "Sender (untrusted metadata):", - "```json", - '{"label":"Chris","name":"Chris","id":"42"}', - "```", - "", - "帮我看看今天的 Oura 数据", - ].join("\n"); - - const jsonlLines = [ - JSON.stringify({ - type: "message", - message: { role: "user", content: envelopedUserText }, - }), - JSON.stringify({ - type: "message", - message: { role: "assistant", content: "好的,我来查一下" }, - }), - ]; - const filePath = path.join(tmpDir, "enveloped-session.jsonl"); - fsSync.writeFileSync(filePath, jsonlLines.join("\n")); - - const entry = await buildSessionEntryWithoutStoreClassification(filePath); - expect(entry).not.toBeNull(); - - const contentLines = entry!.content.split("\n"); - expect(contentLines).toHaveLength(2); - // User line should contain ONLY the real user text, not the JSON envelope. - expect(contentLines[0]).toBe("User: 帮我看看今天的 Oura 数据"); - expect(contentLines[0]).not.toContain("untrusted metadata"); - expect(contentLines[0]).not.toContain("message_id"); - expect(contentLines[0]).not.toContain("```json"); - expect(contentLines[1]).toBe("Assistant: 好的,我来查一下"); - }); - - it("strips inbound metadata when a user envelope is split across text blocks", async () => { - const jsonlLines = [ - JSON.stringify({ - type: "message", - message: { - role: "user", - content: [ - { type: "text", text: "Conversation info (untrusted metadata):" }, - { type: "text", text: "```json" }, - { type: "text", text: '{"message_id":"msg-100","chat_id":"-100123"}' }, - { type: "text", text: "```" }, - { type: "text", text: "" }, - { type: "text", text: "Sender (untrusted metadata):" }, - { type: "text", text: "```json" }, - { type: "text", text: '{"label":"Chris","id":"42"}' }, - { type: "text", text: "```" }, - { type: "text", text: "" }, - { type: "text", text: "Actual user text" }, - ], - }, - }), - ]; - const filePath = path.join(tmpDir, "enveloped-session-array.jsonl"); - fsSync.writeFileSync(filePath, jsonlLines.join("\n")); - - const entry = await buildSessionEntryWithoutStoreClassification(filePath); - expect(entry).not.toBeNull(); - expect(entry!.content).toBe("User: Actual user text"); - }); - - it("wraps pathological long messages into multiple exported lines and repeats mappings", async () => { - const longWordyLine = Array.from({ length: 260 }, (_, idx) => `segment-${idx}`).join(" "); - const timestamp = Date.parse("2026-04-05T10:00:00.000Z"); - const jsonlLines = [ - JSON.stringify({ - type: "message", - timestamp: "2026-04-05T10:00:00.000Z", - message: { role: "user", content: longWordyLine }, - }), - ]; - const filePath = path.join(tmpDir, "wrapped-session.jsonl"); - fsSync.writeFileSync(filePath, jsonlLines.join("\n")); - - const entry = await buildSessionEntryWithoutStoreClassification(filePath); - expect(entry).not.toBeNull(); - - const contentLines = entry!.content.split("\n"); - expect(contentLines.length).toBeGreaterThan(1); - expect(contentLines.every((line) => line.startsWith("User: "))).toBe(true); - expect(contentLines.every((line) => line.length <= 810)).toBe(true); - expect(entry!.lineMap).toEqual(contentLines.map(() => 1)); - expect(entry!.messageTimestampsMs).toEqual(contentLines.map(() => timestamp)); - }); - - it("hard-wraps pathological long tokens without spaces", async () => { - const giantToken = "x".repeat(1800); - const jsonlLines = [ - JSON.stringify({ - type: "message", - message: { role: "assistant", content: giantToken }, - }), - ]; - const filePath = path.join(tmpDir, "hard-wrapped-session.jsonl"); - fsSync.writeFileSync(filePath, jsonlLines.join("\n")); - - const entry = await buildSessionEntryWithoutStoreClassification(filePath); - expect(entry).not.toBeNull(); - - const contentLines = entry!.content.split("\n"); - expect(contentLines.length).toBe(3); - expect(contentLines.every((line) => line.startsWith("Assistant: "))).toBe(true); - expect(contentLines[0].length).toBeLessThanOrEqual(811); - expect(contentLines[1].length).toBeLessThanOrEqual(811); - expect(entry!.lineMap).toEqual([1, 1, 1]); - expect(entry!.messageTimestampsMs).toEqual([0, 0, 0]); - }); - - it("does not split surrogate pairs when hard-wrapping astral unicode without spaces", async () => { - const astralChar = "\u{20000}"; - const giantToken = astralChar.repeat(410); - const jsonlLines = [ - JSON.stringify({ - type: "message", - message: { role: "assistant", content: giantToken }, - }), - ]; - const filePath = path.join(tmpDir, "surrogate-safe-session.jsonl"); - fsSync.writeFileSync(filePath, jsonlLines.join("\n")); - - const entry = await buildSessionEntryWithoutStoreClassification(filePath); - expect(entry).not.toBeNull(); - - const contentLines = entry!.content.split("\n"); - expect(contentLines.length).toBeGreaterThan(1); - expect(entry!.lineMap).toEqual(contentLines.map(() => 1)); - expect(entry!.messageTimestampsMs).toEqual(contentLines.map(() => 0)); - for (const line of contentLines) { - expect(line.startsWith("Assistant: ")).toBe(true); - expectNoUnpairedSurrogates(line); - } - }); - - it("preserves assistant messages that happen to contain sentinel-like text", async () => { - // Assistant role must NOT be stripped — only user messages carry inbound - // envelopes, and assistants may legitimately discuss metadata formats. - const assistantText = - "The envelope format uses 'Conversation info (untrusted metadata):' as a sentinel"; - const jsonlLines = [ - JSON.stringify({ - type: "message", - message: { role: "assistant", content: assistantText }, - }), - ]; - const filePath = path.join(tmpDir, "assistant-sentinel.jsonl"); - fsSync.writeFileSync(filePath, jsonlLines.join("\n")); - - const entry = await buildSessionEntryWithoutStoreClassification(filePath); - expect(entry).not.toBeNull(); - expect(entry!.content).toBe(`Assistant: ${assistantText}`); - }); - - it("flags dreaming narrative transcripts from bootstrap metadata", async () => { - const jsonlLines = [ - JSON.stringify({ - type: "custom", - customType: "openclaw:bootstrap-context:full", - data: { - runId: "dreaming-narrative-light-1775894400455", - sessionId: "sid-1", - }, - }), - JSON.stringify({ - type: "message", - message: { role: "user", content: "Write a dream diary entry from these memory fragments" }, - }), - ]; - const filePath = path.join(tmpDir, "dreaming-session.jsonl"); - fsSync.writeFileSync(filePath, jsonlLines.join("\n")); - - const entry = await buildSessionEntryWithoutStoreClassification(filePath); - - expect(entry).not.toBeNull(); - expect(entry?.generatedByDreamingNarrative).toBe(true); - }); - - it("flags cron run transcripts from the sibling session store and skips their content", async () => { - const sessionsDir = path.join(tmpDir, "agents", "main", "sessions"); - fsSync.mkdirSync(sessionsDir, { recursive: true }); - const filePath = path.join(sessionsDir, "cron-run-session.jsonl"); - fsSync.writeFileSync( - filePath, - [ - JSON.stringify({ - type: "message", - message: { - role: "user", - content: "[cron:job-1 Example] Run the nightly sync", - }, - }), - JSON.stringify({ - type: "message", - message: { - role: "assistant", - content: "Running the nightly sync now.", - }, - }), - ].join("\n"), - ); - fsSync.writeFileSync( - path.join(sessionsDir, "sessions.json"), - JSON.stringify({ - "agent:main:cron:job-1:run:run-1": { - sessionId: "cron-run-session", - sessionFile: filePath, - updatedAt: Date.now(), - }, - }), - "utf-8", - ); - - const entry = await buildSessionEntry(filePath); - - expect(entry).not.toBeNull(); - expect(entry?.generatedByCronRun).toBe(true); - expect(entry?.content).toBe(""); - expect(entry?.lineMap).toEqual([]); - }); - - it("flags dreaming narrative transcripts from the sibling session store before bootstrap lands", async () => { - const sessionsDir = path.join(tmpDir, "agents", "main", "sessions"); - fsSync.mkdirSync(sessionsDir, { recursive: true }); - const filePath = path.join(sessionsDir, "dreaming-session.jsonl"); - fsSync.writeFileSync( - filePath, - [ - JSON.stringify({ - type: "message", - message: { - role: "user", - content: - "Write a dream diary entry from these memory fragments:\n- Candidate: durable note", - }, - }), - JSON.stringify({ - type: "message", - message: { - role: "assistant", - content: "A drifting archive breathed in moonlight.", - }, - }), - ].join("\n"), - ); - fsSync.writeFileSync( - path.join(sessionsDir, "sessions.json"), - JSON.stringify({ - "agent:main:dreaming-narrative-light-1775894400455": { - sessionId: "dreaming-session", - sessionFile: filePath, - updatedAt: Date.now(), - }, - }), - "utf-8", - ); - - const entry = await buildSessionEntry(filePath); - - expect(entry).not.toBeNull(); - expect(entry?.generatedByDreamingNarrative).toBe(true); - expect(entry?.content).toBe(""); - expect(entry?.lineMap).toEqual([]); - }); - - it("does not flag ordinary transcripts that quote the dream-diary prompt", async () => { - const jsonlLines = [ - JSON.stringify({ - type: "message", - message: { - role: "user", - content: - "Write a dream diary entry from these memory fragments:\n- Candidate: durable note", - }, - }), - JSON.stringify({ - type: "message", - message: { role: "assistant", content: "A drifting archive breathed in moonlight." }, - }), - ]; - const filePath = path.join(tmpDir, "dreaming-prompt-session.jsonl"); - fsSync.writeFileSync(filePath, jsonlLines.join("\n")); - - const entry = await buildSessionEntryWithoutStoreClassification(filePath); - - expect(entry).not.toBeNull(); - expect(entry?.generatedByDreamingNarrative).toBeUndefined(); - expect(entry?.content).toContain( - "User: Write a dream diary entry from these memory fragments:", - ); - expect(entry?.content).toContain("Assistant: A drifting archive breathed in moonlight."); - expect(entry?.lineMap).toEqual([1, 2]); - }); - - it("drops generated runtime chatter while preserving real follow-up content", async () => { - const cases = [ - { - name: "system wrapper", - fileName: "system-wrapper-session.jsonl", - records: [ - { - type: "message", - message: { - role: "user", - content: - "System (untrusted): [2026-04-15 14:45:20 PDT] Exec completed (quiet-fo, code 0) :: Converted: 1", - }, - }, - { type: "message", message: { role: "assistant", content: "Handled internally." } }, - { type: "message", message: { role: "user", content: "What changed in the sync?" } }, - { - type: "message", - message: { role: "assistant", content: "One new session was converted." }, - }, - ], - content: [ - "Assistant: Handled internally.", - "User: What changed in the sync?", - "Assistant: One new session was converted.", - ].join("\n"), - lineMap: [2, 3, 4], - }, - { - name: "cron prompt", - fileName: "cron-prompt-session.jsonl", - records: [ - { - type: "message", - message: { role: "user", content: "[cron:job-1 Example] Run the nightly sync" }, - }, - { - type: "message", - message: { role: "assistant", content: "Running the nightly sync now." }, - }, - { - type: "message", - message: { role: "user", content: "Did the nightly sync actually change anything?" }, - }, - { - type: "message", - message: { role: "assistant", content: "No, everything was already current." }, - }, - ], - content: [ - "Assistant: Running the nightly sync now.", - "User: Did the nightly sync actually change anything?", - "Assistant: No, everything was already current.", - ].join("\n"), - lineMap: [2, 3, 4], - }, - { - name: "heartbeat ack", - fileName: "heartbeat-session.jsonl", - records: [ - { - type: "message", - message: { - role: "user", - content: - "Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.", - }, - }, - { type: "message", message: { role: "assistant", content: "HEARTBEAT_OK" } }, - { - type: "message", - message: { role: "user", content: "Summarize what changed in the inbox today." }, - }, - ], - content: "User: Summarize what changed in the inbox today.", - lineMap: [3], - }, - { - name: "internal runtime context", - fileName: "internal-context-session.jsonl", - records: [ - { - type: "message", - message: { - role: "user", - content: [ - "<<>>", - "OpenClaw runtime context (internal):", - "This context is runtime-generated, not user-authored. Keep internal details private.", - "", - "[Internal task completion event]", - "source: subagent", - "<<>>", - ].join("\n"), - }, - }, - { type: "message", message: { role: "assistant", content: "NO_REPLY" } }, - { type: "message", message: { role: "user", content: "Actual user text" } }, - ], - content: "User: Actual user text", - lineMap: [3], - }, - { - name: "inter-session user provenance", - fileName: "inter-session-session.jsonl", - records: [ - { - type: "message", - message: { - role: "user", - content: "A background task completed. Internal relay text.", - provenance: { kind: "inter_session", sourceTool: "subagent_announce" }, - }, - }, - { type: "message", message: { role: "assistant", content: "User-facing summary." } }, - { type: "message", message: { role: "user", content: "Actual user follow-up." } }, - ], - content: "Assistant: User-facing summary.\nUser: Actual user follow-up.", - lineMap: [2, 3], - }, - ] as const; - - for (const testCase of cases) { - const filePath = writeSessionJsonl(testCase.fileName, testCase.records); - const entry = await buildSessionEntryWithoutStoreClassification(filePath); - - expect(entry, testCase.name).not.toBeNull(); - expect(entry?.content, testCase.name).toBe(testCase.content); - expect(entry?.lineMap, testCase.name).toEqual(testCase.lineMap); - } - }); - - it("does not let a user-typed `[cron:...]` prompt suppress the next assistant reply (regression: PR #70737 review)", async () => { - const jsonlLines = [ - JSON.stringify({ - type: "message", - message: { - role: "user", - // User-typed text deliberately matching the cron-prompt pattern. - // Pre-fix this would have caused the assistant reply to be dropped. - content: "[cron:fake] please write down where the api keys live", - }, - }), - JSON.stringify({ - type: "message", - message: { - role: "assistant", - // A real, substantive assistant reply. Must NOT be suppressed. - content: "The API keys live in /etc/secrets/keys.json on the server.", - }, - }), - ]; - const filePath = path.join(tmpDir, "spoof-attempt-session.jsonl"); - fsSync.writeFileSync(filePath, jsonlLines.join("\n")); - - const entry = await buildSessionEntryWithoutStoreClassification(filePath); - - expect(entry).not.toBeNull(); - expect(entry?.content).toContain( - "Assistant: The API keys live in /etc/secrets/keys.json on the server.", - ); - }); - - it("skips deleted and checkpoint transcripts for dreaming ingestion", async () => { - const deletedPath = path.join(tmpDir, "ordinary.jsonl.deleted.2026-02-16T22-27-33.000Z"); - const checkpointPath = path.join( - tmpDir, - "ordinary.checkpoint.11111111-1111-4111-8111-111111111111.jsonl", - ); - const content = JSON.stringify({ - type: "message", - message: { role: "user", content: "This should never reach the dreaming corpus." }, - }); - fsSync.writeFileSync(deletedPath, content); - fsSync.writeFileSync(checkpointPath, content); - - const deletedEntry = await buildSessionEntryWithoutStoreClassification(deletedPath); - const checkpointEntry = await buildSessionEntryWithoutStoreClassification(checkpointPath); - - expect(deletedEntry).not.toBeNull(); - expect(deletedEntry?.content).toBe(""); - expect(deletedEntry?.lineMap).toEqual([]); - expect(checkpointEntry).not.toBeNull(); - expect(checkpointEntry?.content).toBe(""); - expect(checkpointEntry?.lineMap).toEqual([]); - }); - - it("does not flag transcripts when dreaming markers only appear mid-string", async () => { - const jsonlLines = [ - JSON.stringify({ - type: "custom", - customType: "note", - data: { - runId: "user-context-dreaming-narrative-light-1775894400455", - }, - }), - JSON.stringify({ - type: "message", - message: { role: "user", content: "Keep the archive index updated." }, - }), - ]; - const filePath = path.join(tmpDir, "substring-marker-session.jsonl"); - fsSync.writeFileSync(filePath, jsonlLines.join("\n")); - - const entry = await buildSessionEntryWithoutStoreClassification(filePath); - - expect(entry).not.toBeNull(); - expect(entry?.generatedByDreamingNarrative).toBeUndefined(); - expect(entry?.content).toContain("User: Keep the archive index updated."); - expect(entry?.lineMap).toEqual([2]); - }); -}); diff --git a/src/memory-host-sdk/host/session-files.ts b/src/memory-host-sdk/host/session-files.ts deleted file mode 100644 index 0b49b8bb276..00000000000 --- a/src/memory-host-sdk/host/session-files.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "../../../packages/memory-host-sdk/src/host/session-files.js"; diff --git a/src/memory-host-sdk/host/sqlite-vec.ts b/src/memory-host-sdk/host/sqlite-vec.ts deleted file mode 100644 index cd7443af38c..00000000000 --- a/src/memory-host-sdk/host/sqlite-vec.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { DatabaseSync } from "node:sqlite"; -import { formatErrorMessage } from "../../infra/errors.js"; -import { normalizeOptionalString } from "../../shared/string-coerce.js"; - -type SqliteVecModule = { - getLoadablePath: () => string; - load: (db: DatabaseSync) => void; -}; - -const SQLITE_VEC_MODULE_ID = "sqlite-vec"; -let sqliteVecModulePromise: Promise | null = null; - -async function loadSqliteVecModule(): Promise { - sqliteVecModulePromise ??= import(SQLITE_VEC_MODULE_ID) as Promise; - return sqliteVecModulePromise; -} - -export async function loadSqliteVecExtension(params: { - db: DatabaseSync; - extensionPath?: string; -}): Promise<{ ok: boolean; extensionPath?: string; error?: string }> { - try { - const sqliteVec = await loadSqliteVecModule(); - const resolvedPath = normalizeOptionalString(params.extensionPath); - const extensionPath = resolvedPath ?? sqliteVec.getLoadablePath(); - - params.db.enableLoadExtension(true); - if (resolvedPath) { - params.db.loadExtension(extensionPath); - } else { - sqliteVec.load(params.db); - } - - return { ok: true, extensionPath }; - } catch (err) { - const message = formatErrorMessage(err); - return { ok: false, error: message }; - } -} diff --git a/src/memory-host-sdk/host/sqlite.ts b/src/memory-host-sdk/host/sqlite.ts deleted file mode 100644 index 76edebab340..00000000000 --- a/src/memory-host-sdk/host/sqlite.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { createRequire } from "node:module"; -import type { DatabaseSync } from "node:sqlite"; -import { formatErrorMessage } from "../../infra/errors.js"; -import { - configureSqliteWalMaintenance, - type SqliteWalMaintenance, - type SqliteWalMaintenanceOptions, -} from "../../infra/sqlite-wal.js"; -import { installProcessWarningFilter } from "../../infra/warning-filter.js"; - -const require = createRequire(import.meta.url); -const sqliteWalMaintenanceByDb = new WeakMap(); - -export function requireNodeSqlite(): typeof import("node:sqlite") { - installProcessWarningFilter(); - try { - return require("node:sqlite") as typeof import("node:sqlite"); - } catch (err) { - const message = formatErrorMessage(err); - // Node distributions can ship without the experimental builtin SQLite module. - // Surface an actionable error instead of the generic "unknown builtin module". - throw new Error( - `SQLite support is unavailable in this Node runtime (missing node:sqlite). ${message}`, - { cause: err }, - ); - } -} - -export function configureMemorySqliteWalMaintenance( - db: DatabaseSync, - options?: SqliteWalMaintenanceOptions, -): SqliteWalMaintenance { - const existing = sqliteWalMaintenanceByDb.get(db); - if (existing) { - return existing; - } - const maintenance = configureSqliteWalMaintenance(db, options); - sqliteWalMaintenanceByDb.set(db, maintenance); - return maintenance; -} - -export function closeMemorySqliteWalMaintenance(db: DatabaseSync): boolean { - const maintenance = sqliteWalMaintenanceByDb.get(db); - if (!maintenance) { - return true; - } - sqliteWalMaintenanceByDb.delete(db); - return maintenance.close(); -} diff --git a/src/memory-host-sdk/host/status-format.ts b/src/memory-host-sdk/host/status-format.ts deleted file mode 100644 index 0fd70136be0..00000000000 --- a/src/memory-host-sdk/host/status-format.ts +++ /dev/null @@ -1,45 +0,0 @@ -export type Tone = "ok" | "warn" | "muted"; - -export function resolveMemoryVectorState(vector: { enabled: boolean; available?: boolean }): { - tone: Tone; - state: "ready" | "unavailable" | "disabled" | "unknown"; -} { - if (!vector.enabled) { - return { tone: "muted", state: "disabled" }; - } - if (vector.available === true) { - return { tone: "ok", state: "ready" }; - } - if (vector.available === false) { - return { tone: "warn", state: "unavailable" }; - } - return { tone: "muted", state: "unknown" }; -} - -export function resolveMemoryFtsState(fts: { enabled: boolean; available: boolean }): { - tone: Tone; - state: "ready" | "unavailable" | "disabled"; -} { - if (!fts.enabled) { - return { tone: "muted", state: "disabled" }; - } - return fts.available ? { tone: "ok", state: "ready" } : { tone: "warn", state: "unavailable" }; -} - -export function resolveMemoryCacheSummary(cache: { enabled: boolean; entries?: number }): { - tone: Tone; - text: string; -} { - if (!cache.enabled) { - return { tone: "muted", text: "cache off" }; - } - const suffix = typeof cache.entries === "number" ? ` (${cache.entries})` : ""; - return { tone: "ok", text: `cache on${suffix}` }; -} - -export function resolveMemoryCacheState(cache: { enabled: boolean }): { - tone: Tone; - state: "enabled" | "disabled"; -} { - return cache.enabled ? { tone: "ok", state: "enabled" } : { tone: "muted", state: "disabled" }; -} diff --git a/test/scripts/test-projects.test.ts b/test/scripts/test-projects.test.ts index 85afb70fce1..53d86c04489 100644 --- a/test/scripts/test-projects.test.ts +++ b/test/scripts/test-projects.test.ts @@ -591,13 +591,12 @@ describe("scripts/test-projects changed-target routing", () => { resolveChangedTestTargetPlan([ "src/commands/doctor-memory-search.ts", "src/memory-host-sdk/host/embedding-defaults.ts", - "src/memory-host-sdk/host/embeddings.ts", ]), ).toEqual({ mode: "targets", targets: [ "src/commands/doctor-memory-search.test.ts", - "src/memory-host-sdk/host/embeddings.test.ts", + "packages/memory-host-sdk/src/host/embeddings.test.ts", ], }); }); diff --git a/test/vitest-unit-fast-config.test.ts b/test/vitest-unit-fast-config.test.ts index 90fdf754c7f..f9882478c54 100644 --- a/test/vitest-unit-fast-config.test.ts +++ b/test/vitest-unit-fast-config.test.ts @@ -32,7 +32,7 @@ describe("unit-fast vitest lane", () => { expect(config.test?.include).toContain("src/crestodian/rescue-policy.test.ts"); expect(config.test?.include).toContain("src/crestodian/assistant.configured.test.ts"); expect(config.test?.include).toContain("src/flows/search-setup.test.ts"); - expect(config.test?.include).toContain("src/memory-host-sdk/host/mirror.test.ts"); + expect(config.test?.include).toContain("src/memory-host-sdk/host/backend-config.test.ts"); expect(config.test?.include).toContain("src/plugins/config-policy.test.ts"); expect(config.test?.include).toContain("src/proxy-capture/proxy-server.test.ts"); expect(config.test?.include).toContain("src/realtime-voice/agent-consult-tool.test.ts"); diff --git a/test/vitest/vitest.unit-fast-paths.mjs b/test/vitest/vitest.unit-fast-paths.mjs index 362e83e48b7..c1ee6a2f517 100644 --- a/test/vitest/vitest.unit-fast-paths.mjs +++ b/test/vitest/vitest.unit-fast-paths.mjs @@ -108,14 +108,7 @@ export const forcedUnitFastTestFiles = [ "src/install-sh-version.test.ts", "src/logger.test.ts", "src/library.test.ts", - "src/memory-host-sdk/host/embeddings.test.ts", - "src/memory-host-sdk/host/internal.test.ts", - "src/memory-host-sdk/host/batch-http.test.ts", "src/memory-host-sdk/host/backend-config.test.ts", - "src/memory-host-sdk/host/embeddings-remote-fetch.test.ts", - "src/memory-host-sdk/host/mirror.test.ts", - "src/memory-host-sdk/host/post-json.test.ts", - "src/memory-host-sdk/host/session-files.test.ts", "src/media-generation/provider-capabilities.contract.test.ts", "src/music-generation/runtime.test.ts", "src/mcp/channel-server.shutdown-unhandled-rejection.test.ts",