mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-09 03:22:40 +00:00
docs: document memory host runtime helpers
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import { createLocalEmbeddingProviderInProcess } from "./embeddings.js";
|
||||
import type { EmbeddingProvider, EmbeddingProviderOptions } from "./embeddings.types.js";
|
||||
|
||||
// Child process entrypoint for local embedding work.
|
||||
|
||||
/** Request payloads accepted from the parent worker client. */
|
||||
type LocalEmbeddingWorkerRequest =
|
||||
| {
|
||||
id: number;
|
||||
@@ -24,6 +27,7 @@ type LocalEmbeddingWorkerRequest =
|
||||
type: "close";
|
||||
};
|
||||
|
||||
/** Serialized error shape returned over JSON IPC. */
|
||||
type LocalEmbeddingWorkerSerializedError = {
|
||||
message: string;
|
||||
code?: string;
|
||||
@@ -33,12 +37,14 @@ let provider: EmbeddingProvider | null = null;
|
||||
let providerOptionsKey: string | null = null;
|
||||
let requestQueue: Promise<void> = Promise.resolve();
|
||||
|
||||
/** Send one JSON IPC message when the child still has an IPC channel. */
|
||||
function send(message: unknown): void {
|
||||
if (typeof process.send === "function") {
|
||||
process.send(message);
|
||||
}
|
||||
}
|
||||
|
||||
/** Reuse the current provider while options are unchanged, otherwise rebuild it. */
|
||||
async function getProvider(options: EmbeddingProviderOptions): Promise<EmbeddingProvider> {
|
||||
const key = JSON.stringify(options);
|
||||
if (provider && providerOptionsKey === key) {
|
||||
@@ -50,6 +56,7 @@ async function getProvider(options: EmbeddingProviderOptions): Promise<Embedding
|
||||
return provider;
|
||||
}
|
||||
|
||||
/** Close and forget the active in-process provider. */
|
||||
async function closeProvider(): Promise<void> {
|
||||
const current = provider;
|
||||
provider = null;
|
||||
@@ -57,6 +64,7 @@ async function closeProvider(): Promise<void> {
|
||||
await current?.close?.();
|
||||
}
|
||||
|
||||
/** Preserve error message and code across JSON IPC. */
|
||||
function serializeError(err: unknown): LocalEmbeddingWorkerSerializedError {
|
||||
if (!(err instanceof Error)) {
|
||||
return { message: String(err) };
|
||||
@@ -68,6 +76,7 @@ function serializeError(err: unknown): LocalEmbeddingWorkerSerializedError {
|
||||
};
|
||||
}
|
||||
|
||||
/** Handle one parent request after queue serialization. */
|
||||
async function handleRequest(request: LocalEmbeddingWorkerRequest): Promise<void> {
|
||||
if (request.type === "close") {
|
||||
await closeProvider();
|
||||
@@ -90,6 +99,7 @@ async function handleRequest(request: LocalEmbeddingWorkerRequest): Promise<void
|
||||
send({ id: request.id, ok: true, value });
|
||||
}
|
||||
|
||||
// Requests are serialized so node-llama-cpp context state is not used concurrently.
|
||||
process.on("message", (message) => {
|
||||
const request = message as LocalEmbeddingWorkerRequest;
|
||||
requestQueue = requestQueue.then(async () => {
|
||||
@@ -101,6 +111,7 @@ process.on("message", (message) => {
|
||||
});
|
||||
});
|
||||
|
||||
// Parent disconnect means the worker is orphaned; close provider resources before exiting.
|
||||
process.once("disconnect", () => {
|
||||
void closeProvider().finally(() => {
|
||||
process.exit(0);
|
||||
|
||||
@@ -14,6 +14,9 @@ import type {
|
||||
} from "./embeddings.types.js";
|
||||
import { normalizeOptionalString } from "./string-utils.js";
|
||||
|
||||
// Parent-side local embedding worker client for isolating node-llama-cpp state.
|
||||
|
||||
/** Request payloads sent from the parent process to the local embedding worker child. */
|
||||
type LocalEmbeddingWorkerRequestPayload =
|
||||
| {
|
||||
type: "initialize";
|
||||
@@ -35,6 +38,7 @@ type LocalEmbeddingWorkerRequestPayload =
|
||||
|
||||
type LocalEmbeddingWorkerRequest = LocalEmbeddingWorkerRequestPayload & { id: number };
|
||||
|
||||
/** Response payloads sent from the local embedding worker child back to the parent. */
|
||||
type LocalEmbeddingWorkerResponse =
|
||||
| {
|
||||
id: number;
|
||||
@@ -52,12 +56,14 @@ type LocalEmbeddingWorkerResponse =
|
||||
};
|
||||
};
|
||||
|
||||
/** Pending parent request plus abort cleanup. */
|
||||
type PendingRequest = {
|
||||
resolve: (value: number[] | number[][] | undefined) => void;
|
||||
reject: (err: unknown) => void;
|
||||
abort?: () => void;
|
||||
};
|
||||
|
||||
/** Resolve the worker child script for source, package, and bundled runtime layouts. */
|
||||
function resolveDefaultWorkerScriptPath(): string {
|
||||
const currentPath = fileURLToPath(import.meta.url);
|
||||
const extension = path.extname(currentPath);
|
||||
@@ -71,6 +77,7 @@ function resolveDefaultWorkerScriptPath(): string {
|
||||
return path.join(path.dirname(currentPath), sibling);
|
||||
}
|
||||
|
||||
/** Keep only local embedding options that are safe and necessary to send over IPC. */
|
||||
function serializeLocalEmbeddingOptions(
|
||||
options: EmbeddingProviderOptions,
|
||||
): EmbeddingProviderOptions {
|
||||
@@ -83,6 +90,7 @@ function serializeLocalEmbeddingOptions(
|
||||
};
|
||||
}
|
||||
|
||||
/** Create a typed failure for unexpected worker process exits. */
|
||||
function createWorkerExitError(code: number | null, signal: NodeJS.Signals | null): Error {
|
||||
const detail = signal ? `signal ${signal}` : `exit code ${code ?? "unknown"}`;
|
||||
return createLocalEmbeddingWorkerFailureError({
|
||||
@@ -94,6 +102,7 @@ function createWorkerExitError(code: number | null, signal: NodeJS.Signals | nul
|
||||
});
|
||||
}
|
||||
|
||||
/** Convert worker response errors into Error objects while preserving worker error codes. */
|
||||
function createWorkerResponseError(error: LocalEmbeddingWorkerResponse & { ok: false }): Error {
|
||||
if (typeof error.error === "object" && error.error) {
|
||||
const message = error.error.message || "Local embedding worker failed";
|
||||
@@ -128,6 +137,7 @@ const WORKER_UNSAFE_EXEC_ARGV_OPTION_PREFIXES = [
|
||||
|
||||
const WORKER_CLOSE_GRACE_MS = 250;
|
||||
|
||||
/** Drop execArgv flags that would make forked workers debug/eval stateful or unsafe. */
|
||||
function resolveWorkerExecArgv(): string[] {
|
||||
const args: string[] = [];
|
||||
let skipNext = false;
|
||||
@@ -151,6 +161,7 @@ function resolveWorkerExecArgv(): string[] {
|
||||
return args;
|
||||
}
|
||||
|
||||
/** IPC client that serializes local embedding calls through one child process. */
|
||||
class LocalEmbeddingWorkerClient {
|
||||
private child: ChildProcess | null = null;
|
||||
private nextRequestId = 1;
|
||||
@@ -158,10 +169,12 @@ class LocalEmbeddingWorkerClient {
|
||||
|
||||
constructor(private readonly scriptPath: string) {}
|
||||
|
||||
/** Start or reuse the child worker and initialize its provider. */
|
||||
async initialize(options: EmbeddingProviderOptions): Promise<void> {
|
||||
await this.send({ type: "initialize", options });
|
||||
}
|
||||
|
||||
/** Request one query embedding from the child worker. */
|
||||
async embedQuery(
|
||||
options: EmbeddingProviderOptions,
|
||||
text: string,
|
||||
@@ -171,6 +184,7 @@ class LocalEmbeddingWorkerClient {
|
||||
return Array.isArray(result) ? (result as number[]) : [];
|
||||
}
|
||||
|
||||
/** Request a batch of embeddings from the child worker. */
|
||||
async embedBatch(
|
||||
options: EmbeddingProviderOptions,
|
||||
texts: string[],
|
||||
@@ -180,6 +194,7 @@ class LocalEmbeddingWorkerClient {
|
||||
return Array.isArray(result) ? (result as number[][]) : [];
|
||||
}
|
||||
|
||||
/** Ask the child to close gracefully, then force shutdown after a short grace period. */
|
||||
async close(): Promise<void> {
|
||||
const child = this.child;
|
||||
if (!child) {
|
||||
@@ -204,6 +219,7 @@ class LocalEmbeddingWorkerClient {
|
||||
}
|
||||
}
|
||||
|
||||
/** Ensure the child process exists and has lifecycle failure handlers installed. */
|
||||
private ensureChild(): ChildProcess {
|
||||
if (this.child?.connected) {
|
||||
return this.child;
|
||||
@@ -238,6 +254,7 @@ class LocalEmbeddingWorkerClient {
|
||||
return child;
|
||||
}
|
||||
|
||||
/** Send one request over IPC and bind its abort signal to child shutdown. */
|
||||
private async send(
|
||||
request: LocalEmbeddingWorkerRequestPayload,
|
||||
options?: EmbeddingProviderCallOptions,
|
||||
@@ -280,6 +297,7 @@ class LocalEmbeddingWorkerClient {
|
||||
});
|
||||
}
|
||||
|
||||
/** Route one worker response to the matching pending request. */
|
||||
private handleMessage(message: unknown): void {
|
||||
const response = message as Partial<LocalEmbeddingWorkerResponse>;
|
||||
if (typeof response.id !== "number") {
|
||||
@@ -300,6 +318,7 @@ class LocalEmbeddingWorkerClient {
|
||||
);
|
||||
}
|
||||
|
||||
/** Disconnect and kill the current child process if it is still alive. */
|
||||
private shutdownChild(): void {
|
||||
const child = this.child;
|
||||
this.child = null;
|
||||
@@ -314,6 +333,7 @@ class LocalEmbeddingWorkerClient {
|
||||
}
|
||||
}
|
||||
|
||||
/** Reject all pending requests after child process failure. */
|
||||
private rejectPending(err: unknown): void {
|
||||
const pending = [...this.pending.values()];
|
||||
this.pending.clear();
|
||||
@@ -324,6 +344,7 @@ class LocalEmbeddingWorkerClient {
|
||||
}
|
||||
}
|
||||
|
||||
/** Create the public local embedding provider backed by the child worker client. */
|
||||
export async function createLocalEmbeddingWorkerProvider(
|
||||
options: EmbeddingProviderOptions,
|
||||
runtimeOptions?: LocalEmbeddingProviderRuntimeOptions,
|
||||
@@ -368,6 +389,7 @@ export async function createLocalEmbeddingWorkerProvider(
|
||||
};
|
||||
}
|
||||
|
||||
/** Convert abort reasons or arbitrary thrown values into lint-safe Error objects. */
|
||||
function toLintErrorObject(value: unknown, fallbackMessage: string): Error {
|
||||
if (value instanceof Error) {
|
||||
return value;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import { formatErrorMessage } from "./error-utils.js";
|
||||
|
||||
// SQLite schema setup for builtin memory index, embedding cache, and FTS.
|
||||
|
||||
/** Ensure memory index tables and optional FTS/cache tables exist. */
|
||||
export function ensureMemoryIndexSchema(params: {
|
||||
db: DatabaseSync;
|
||||
embeddingCacheTable: string;
|
||||
@@ -89,6 +92,7 @@ export function ensureMemoryIndexSchema(params: {
|
||||
return { ftsAvailable, ...(ftsError ? { ftsError } : {}) };
|
||||
}
|
||||
|
||||
/** Add a missing shipped column without rebuilding existing memory tables. */
|
||||
function ensureColumn(
|
||||
db: DatabaseSync,
|
||||
table: "files" | "chunks",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { normalizeLowercaseStringOrEmpty } from "./string-utils.js";
|
||||
|
||||
// Multimodal memory settings and file classification helpers.
|
||||
|
||||
const MEMORY_MULTIMODAL_SPECS = {
|
||||
image: {
|
||||
labelPrefix: "Image file",
|
||||
@@ -11,20 +13,26 @@ const MEMORY_MULTIMODAL_SPECS = {
|
||||
},
|
||||
} as const;
|
||||
|
||||
/** Supported multimodal memory modality. */
|
||||
export type MemoryMultimodalModality = keyof typeof MEMORY_MULTIMODAL_SPECS;
|
||||
/** All supported multimodal memory modalities in stable config order. */
|
||||
export const MEMORY_MULTIMODAL_MODALITIES = Object.keys(
|
||||
MEMORY_MULTIMODAL_SPECS,
|
||||
) as MemoryMultimodalModality[];
|
||||
/** User selection for one modality or all modalities. */
|
||||
export type MemoryMultimodalSelection = MemoryMultimodalModality | "all";
|
||||
|
||||
/** Normalized multimodal memory ingestion settings. */
|
||||
export type MemoryMultimodalSettings = {
|
||||
enabled: boolean;
|
||||
modalities: MemoryMultimodalModality[];
|
||||
maxFileBytes: number;
|
||||
};
|
||||
|
||||
/** Default max bytes for one multimodal memory file. */
|
||||
export const DEFAULT_MEMORY_MULTIMODAL_MAX_FILE_BYTES = 10 * 1024 * 1024;
|
||||
|
||||
/** Normalize user modality selections to supported modalities. */
|
||||
export function normalizeMemoryMultimodalModalities(
|
||||
raw: MemoryMultimodalSelection[] | undefined,
|
||||
): MemoryMultimodalModality[] {
|
||||
@@ -40,6 +48,7 @@ export function normalizeMemoryMultimodalModalities(
|
||||
return Array.from(normalized);
|
||||
}
|
||||
|
||||
/** Normalize user multimodal settings, including disabled-state empty modality list. */
|
||||
export function normalizeMemoryMultimodalSettings(raw: {
|
||||
enabled?: boolean;
|
||||
modalities?: MemoryMultimodalSelection[];
|
||||
@@ -57,16 +66,19 @@ export function normalizeMemoryMultimodalSettings(raw: {
|
||||
};
|
||||
}
|
||||
|
||||
/** Return true when multimodal memory ingestion has at least one enabled modality. */
|
||||
export function isMemoryMultimodalEnabled(settings: MemoryMultimodalSettings): boolean {
|
||||
return settings.enabled && settings.modalities.length > 0;
|
||||
}
|
||||
|
||||
/** Return accepted file extensions for a modality. */
|
||||
export function getMemoryMultimodalExtensions(
|
||||
modality: MemoryMultimodalModality,
|
||||
): readonly string[] {
|
||||
return MEMORY_MULTIMODAL_SPECS[modality].extensions;
|
||||
}
|
||||
|
||||
/** Build the text label that accompanies embedded multimodal file content. */
|
||||
export function buildMemoryMultimodalLabel(
|
||||
modality: MemoryMultimodalModality,
|
||||
normalizedPath: string,
|
||||
@@ -74,6 +86,7 @@ export function buildMemoryMultimodalLabel(
|
||||
return `${MEMORY_MULTIMODAL_SPECS[modality].labelPrefix}: ${normalizedPath}`;
|
||||
}
|
||||
|
||||
/** Build a glob that matches an extension case-insensitively for QMD sources. */
|
||||
export function buildCaseInsensitiveExtensionGlob(extension: string): string {
|
||||
const normalized = normalizeLowercaseStringOrEmpty(extension).replace(/^\./, "");
|
||||
if (!normalized) {
|
||||
@@ -83,6 +96,7 @@ export function buildCaseInsensitiveExtensionGlob(extension: string): string {
|
||||
return `*.${parts.join("")}`;
|
||||
}
|
||||
|
||||
/** Classify a file path into a supported multimodal modality under current settings. */
|
||||
export function classifyMemoryMultimodalPath(
|
||||
filePath: string,
|
||||
settings: MemoryMultimodalSettings,
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
// Minimal node-llama-cpp type facade used by the local embedding provider.
|
||||
|
||||
/** Embedding vector returned by node-llama-cpp. */
|
||||
export type LlamaEmbedding = {
|
||||
vector: Float32Array | number[];
|
||||
};
|
||||
|
||||
/** Embedding context created from a loaded llama model. */
|
||||
export type LlamaEmbeddingContext = {
|
||||
getEmbeddingFor: (text: string) => Promise<LlamaEmbedding>;
|
||||
dispose?: () => Promise<void> | void;
|
||||
};
|
||||
|
||||
/** Loaded llama model capable of creating embedding contexts. */
|
||||
export type LlamaModel = {
|
||||
createEmbeddingContext: (options?: {
|
||||
contextSize?: number | "auto";
|
||||
@@ -15,16 +20,19 @@ export type LlamaModel = {
|
||||
dispose?: () => Promise<void> | void;
|
||||
};
|
||||
|
||||
/** Options accepted by node-llama-cpp model file resolution. */
|
||||
export type ResolveModelFileOptions = {
|
||||
directory?: string;
|
||||
signal?: AbortSignal;
|
||||
};
|
||||
|
||||
/** Root llama runtime object exposed by node-llama-cpp. */
|
||||
export type Llama = {
|
||||
loadModel: (params: { modelPath: string; loadSignal?: AbortSignal }) => Promise<LlamaModel>;
|
||||
dispose?: () => Promise<void> | void;
|
||||
};
|
||||
|
||||
/** Imported node-llama-cpp module shape used by local embeddings. */
|
||||
export type NodeLlamaCppModule = {
|
||||
LlamaLogLevel: {
|
||||
error: number;
|
||||
@@ -38,6 +46,7 @@ export type NodeLlamaCppModule = {
|
||||
|
||||
const NODE_LLAMA_CPP_MODULE = "node-llama-cpp";
|
||||
|
||||
/** Dynamically import node-llama-cpp so the optional dependency is loaded only when needed. */
|
||||
export async function importNodeLlamaCpp() {
|
||||
return import(NODE_LLAMA_CPP_MODULE) as Promise<NodeLlamaCppModule>;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { requireApiKey } from "../../../../src/agents/model-auth-runtime-shared.js";
|
||||
import type { resolveApiKeyForProvider as ResolveApiKeyForProvider } from "../../../../src/agents/model-auth.js";
|
||||
|
||||
// Lazy auth facade so memory host helpers avoid eager model-auth module loading.
|
||||
|
||||
export { requireApiKey };
|
||||
|
||||
/** Resolve a provider API key through the core model-auth runtime. */
|
||||
export const resolveApiKeyForProvider: typeof ResolveApiKeyForProvider = async (...args) => {
|
||||
const auth = await import("../../../../src/agents/model-auth.js");
|
||||
return auth.resolveApiKeyForProvider(...args);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Narrow CLI/runtime facade re-exported for memory host helpers.
|
||||
|
||||
export {
|
||||
colorize,
|
||||
defaultRuntime,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Narrow IO/runtime facade re-exported for memory host helpers.
|
||||
|
||||
export {
|
||||
CHARS_PER_TOKEN_ESTIMATE,
|
||||
DEFAULT_SQLITE_WAL_AUTOCHECKPOINT_PAGES,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Narrow network/runtime facade re-exported for memory remote HTTP helpers.
|
||||
|
||||
export { fetchWithSsrFGuard } from "../../../../src/infra/net/fetch-guard.js";
|
||||
export { shouldUseEnvHttpProxyForUrl } from "../../../../src/infra/net/proxy-env.js";
|
||||
export { ssrfPolicyFromHttpBaseUrlAllowedHostname } from "../../../../src/infra/net/ssrf.js";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Narrow session/runtime facade re-exported for memory transcript helpers.
|
||||
|
||||
export {
|
||||
HEARTBEAT_PROMPT,
|
||||
HEARTBEAT_TOKEN,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { formatErrorMessage } from "./error-utils.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "./string-utils.js";
|
||||
|
||||
// Parser for qmd query JSON output, including noisy CLI wrapper output.
|
||||
|
||||
/** Normalized qmd query result consumed by memory search. */
|
||||
export type QmdQueryResult = {
|
||||
docid?: string;
|
||||
score?: number;
|
||||
@@ -12,6 +15,7 @@ export type QmdQueryResult = {
|
||||
endLine?: number;
|
||||
};
|
||||
|
||||
/** Parse qmd stdout/stderr into normalized results, accepting known no-result markers. */
|
||||
export function parseQmdQueryJson(stdout: string, stderr: string): QmdQueryResult[] {
|
||||
const trimmedStdout = stdout.trim();
|
||||
const trimmedStderr = stderr.trim();
|
||||
@@ -47,6 +51,7 @@ export function parseQmdQueryJson(stdout: string, stderr: string): QmdQueryResul
|
||||
}
|
||||
}
|
||||
|
||||
/** Emit parse warnings outside tests so broken qmd output is visible to operators. */
|
||||
function warnQmdQueryParseError(message: string): void {
|
||||
if (process.env.VITEST || process.env.NODE_ENV === "test") {
|
||||
return;
|
||||
@@ -54,6 +59,7 @@ function warnQmdQueryParseError(message: string): void {
|
||||
process.stderr.write(`qmd query returned invalid JSON: ${message}\n`);
|
||||
}
|
||||
|
||||
/** Detect qmd no-result marker output on stdout or stderr. */
|
||||
function isQmdNoResultsOutput(raw: string): boolean {
|
||||
const lines = raw
|
||||
.split(/\r?\n/)
|
||||
@@ -62,6 +68,7 @@ function isQmdNoResultsOutput(raw: string): boolean {
|
||||
return lines.some((line) => isQmdNoResultsLine(line));
|
||||
}
|
||||
|
||||
/** Match qmd no-result lines with optional warning/info prefixes. */
|
||||
function isQmdNoResultsLine(line: string): boolean {
|
||||
if (line === "no results found" || line === "no results found.") {
|
||||
return true;
|
||||
@@ -71,10 +78,12 @@ function isQmdNoResultsLine(line: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/** Bound stderr context included in parse errors. */
|
||||
function summarizeQmdStderr(raw: string): string {
|
||||
return raw.length <= 120 ? raw : `${raw.slice(0, 117)}...`;
|
||||
}
|
||||
|
||||
/** Parse and normalize a strict qmd JSON array payload. */
|
||||
function parseQmdQueryResultArray(raw: string): QmdQueryResult[] | null {
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
@@ -111,10 +120,12 @@ function parseQmdQueryResultArray(raw: string): QmdQueryResult[] | null {
|
||||
}
|
||||
}
|
||||
|
||||
/** Normalize qmd line numbers, rejecting zero, negative, and non-integer values. */
|
||||
function parseQmdLineNumber(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
||||
}
|
||||
|
||||
/** Extract the first complete JSON array from noisy stdout. */
|
||||
function extractFirstJsonArray(raw: string): string | null {
|
||||
const start = raw.indexOf("[");
|
||||
if (start < 0) {
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import type { MemoryReadResult } from "./types.js";
|
||||
|
||||
// Shared memory-file read result shaping and truncation notices.
|
||||
|
||||
/** Default number of lines returned by memory read helpers. */
|
||||
export const DEFAULT_MEMORY_READ_LINES = 120;
|
||||
/** Default max character budget for memory read helper output. */
|
||||
export const DEFAULT_MEMORY_READ_MAX_CHARS = 12_000;
|
||||
|
||||
export type { MemoryReadResult } from "./types.js";
|
||||
|
||||
/** Build the continuation notice appended to truncated memory excerpts. */
|
||||
function buildContinuationNotice(params: {
|
||||
nextFrom: number | undefined;
|
||||
suggestReadFallback?: boolean;
|
||||
@@ -19,6 +24,7 @@ function buildContinuationNotice(params: {
|
||||
return `\n\n${base.slice(0, -1)}${fallback}]`;
|
||||
}
|
||||
|
||||
/** Fit line slices to the response character budget while preserving line boundaries. */
|
||||
function fitLinesToCharBudget(params: { lines: string[]; maxChars: number }): {
|
||||
text: string;
|
||||
includedLines: number;
|
||||
@@ -47,12 +53,14 @@ function fitLinesToCharBudget(params: { lines: string[]; maxChars: number }): {
|
||||
};
|
||||
}
|
||||
|
||||
/** Normalize optional numeric config to a positive integer fallback. */
|
||||
function normalizePositiveInteger(value: number | undefined, fallback: number): number {
|
||||
return typeof value === "number" && Number.isFinite(value)
|
||||
? Math.max(1, Math.floor(value))
|
||||
: fallback;
|
||||
}
|
||||
|
||||
/** Build a memory read result from an already-selected line slice. */
|
||||
export function buildMemoryReadResultFromSlice(params: {
|
||||
selectedLines: string[];
|
||||
relPath: string;
|
||||
@@ -92,6 +100,7 @@ export function buildMemoryReadResultFromSlice(params: {
|
||||
};
|
||||
}
|
||||
|
||||
/** Build a memory read result from raw file content and caller range options. */
|
||||
export function buildMemoryReadResult(params: {
|
||||
content: string;
|
||||
relPath: string;
|
||||
|
||||
@@ -23,6 +23,9 @@ import {
|
||||
} from "./read-file-shared.js";
|
||||
import { retryTransientMemoryRead } from "./read-retry.js";
|
||||
|
||||
// Secure markdown memory-file reader for workspace and configured extra paths.
|
||||
|
||||
/** Check that an absolute path stays inside an allowed extra directory without symlink escapes. */
|
||||
async function isAllowedAdditionalDirectoryPath(
|
||||
additionalPath: string,
|
||||
absPath: string,
|
||||
@@ -46,6 +49,7 @@ async function isAllowedAdditionalDirectoryPath(
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Return true when a file vanished after path validation but before content read. */
|
||||
function isFileDisappearedDuringReadError(err: unknown): boolean {
|
||||
return (
|
||||
isFileMissingError(err) ||
|
||||
@@ -58,6 +62,7 @@ function isFileDisappearedDuringReadError(err: unknown): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/** Read a validated memory markdown file from workspace or configured extra paths. */
|
||||
export async function readMemoryFile(params: {
|
||||
workspaceDir: string;
|
||||
extraPaths?: string[];
|
||||
@@ -112,6 +117,7 @@ export async function readMemoryFile(params: {
|
||||
}
|
||||
if (allowedWorkspace) {
|
||||
try {
|
||||
// Workspace reads use the safe fs root so symlink escapes are rejected before file IO.
|
||||
const workspaceRoot = await root(params.workspaceDir);
|
||||
await workspaceRoot.resolve(relPath);
|
||||
} catch (err) {
|
||||
@@ -150,6 +156,7 @@ export async function readMemoryFile(params: {
|
||||
});
|
||||
}
|
||||
|
||||
/** Resolve agent memory config and read one memory file for that agent. */
|
||||
export async function readAgentMemoryFile(params: {
|
||||
cfg: OpenClawConfig;
|
||||
agentId: string;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
// Resolves optional sqlite-vec native extension packages for the current platform.
|
||||
|
||||
/** Package/file pair for one sqlite-vec native platform build. */
|
||||
type PlatformVariant = { readonly pkg: string; readonly file: string };
|
||||
|
||||
const PLATFORM_VARIANTS: Readonly<Record<string, PlatformVariant | undefined>> = {
|
||||
@@ -10,6 +13,7 @@ const PLATFORM_VARIANTS: Readonly<Record<string, PlatformVariant | undefined>> =
|
||||
"win32-x64": { pkg: "sqlite-vec-windows-x64", file: "vec0.dll" },
|
||||
};
|
||||
|
||||
/** Resolve the installed sqlite-vec native extension for the current platform if present. */
|
||||
export function resolveSqliteVecPlatformVariant():
|
||||
| { pkg: string; extensionPath: string }
|
||||
| undefined {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Public SQLite WAL maintenance facade for memory database callers.
|
||||
|
||||
export {
|
||||
DEFAULT_SQLITE_WAL_AUTOCHECKPOINT_PAGES,
|
||||
DEFAULT_SQLITE_WAL_TRUNCATE_INTERVAL_MS,
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
// Public process warning filter facade for memory host callers.
|
||||
|
||||
export { installProcessWarningFilter, shouldIgnoreWarning } from "./openclaw-runtime-io.js";
|
||||
export type { ProcessWarning } from "./openclaw-runtime-io.js";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Public Windows spawn facade for memory host callers.
|
||||
|
||||
export {
|
||||
materializeWindowsSpawnProgram,
|
||||
resolveWindowsSpawnProgram,
|
||||
|
||||
Reference in New Issue
Block a user