@openclaw/memory-lancedb 2026.7.2-beta.4 → 2026.7.2-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli-metadata.js +3 -1
- package/dist/cli-output-mode.js +13 -0
- package/dist/config.js +2 -18
- package/dist/doctor-contract-api.js +76 -1
- package/dist/embeddings.js +312 -0
- package/dist/index.js +40 -878
- package/dist/lancedb-store.js +32 -16
- package/dist/memory-capture-sanitization.js +346 -0
- package/dist/memory-cli.js +134 -0
- package/dist/memory-policy.js +145 -0
- package/package.json +6 -7
- package/npm-shrinkwrap.json +0 -481
package/dist/cli-metadata.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isMemoryMachineOutput } from "./cli-output-mode.js";
|
|
1
2
|
import { definePluginEntry } from "openclaw/plugin-sdk/core";
|
|
2
3
|
//#region extensions/memory-lancedb/cli-metadata.ts
|
|
3
4
|
var cli_metadata_default = definePluginEntry({
|
|
@@ -8,7 +9,8 @@ var cli_metadata_default = definePluginEntry({
|
|
|
8
9
|
api.registerCli(() => {}, { descriptors: [{
|
|
9
10
|
name: "ltm",
|
|
10
11
|
description: "Inspect and query LanceDB-backed memory",
|
|
11
|
-
hasSubcommands: true
|
|
12
|
+
hasSubcommands: true,
|
|
13
|
+
machineOutput: isMemoryMachineOutput
|
|
12
14
|
}] });
|
|
13
15
|
}
|
|
14
16
|
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { getRootOptionAwareCommandPath } from "openclaw/plugin-sdk/cli-argv";
|
|
2
|
+
//#region extensions/memory-lancedb/cli-output-mode.ts
|
|
3
|
+
/** LanceDB inspection commands emit JSON as their only presentation. */
|
|
4
|
+
function isMemoryMachineOutput(params) {
|
|
5
|
+
const [, command] = getRootOptionAwareCommandPath(params.argv, 2);
|
|
6
|
+
return [
|
|
7
|
+
"list",
|
|
8
|
+
"query",
|
|
9
|
+
"search"
|
|
10
|
+
].includes(command ?? "");
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
export { isMemoryMachineOutput };
|
package/dist/config.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { parseFiniteNumber } from "openclaw/plugin-sdk/number-runtime";
|
|
2
|
-
import fs from "node:fs";
|
|
3
1
|
import { homedir } from "node:os";
|
|
4
2
|
import { join } from "node:path";
|
|
3
|
+
import { parseFiniteNumber } from "openclaw/plugin-sdk/number-runtime";
|
|
5
4
|
//#region extensions/memory-lancedb/config.ts
|
|
6
5
|
const MEMORY_CATEGORIES = [
|
|
7
6
|
"preference",
|
|
@@ -13,22 +12,7 @@ const MEMORY_CATEGORIES = [
|
|
|
13
12
|
const DEFAULT_MODEL = "text-embedding-3-small";
|
|
14
13
|
const DEFAULT_CAPTURE_MAX_CHARS = 500;
|
|
15
14
|
const DEFAULT_RECALL_MAX_CHARS = 1e3;
|
|
16
|
-
const
|
|
17
|
-
function resolveDefaultDbPath() {
|
|
18
|
-
const home = homedir();
|
|
19
|
-
const preferred = join(home, ".openclaw", "memory", "lancedb");
|
|
20
|
-
try {
|
|
21
|
-
if (fs.existsSync(preferred)) return preferred;
|
|
22
|
-
} catch {}
|
|
23
|
-
for (const legacy of LEGACY_STATE_DIRS) {
|
|
24
|
-
const candidate = join(home, legacy, "memory", "lancedb");
|
|
25
|
-
try {
|
|
26
|
-
if (fs.existsSync(candidate)) return candidate;
|
|
27
|
-
} catch {}
|
|
28
|
-
}
|
|
29
|
-
return preferred;
|
|
30
|
-
}
|
|
31
|
-
const DEFAULT_DB_PATH = resolveDefaultDbPath();
|
|
15
|
+
const DEFAULT_DB_PATH = join(homedir(), ".openclaw", "memory", "lancedb");
|
|
32
16
|
const EMBEDDING_DIMENSIONS = {
|
|
33
17
|
"text-embedding-3-small": 1536,
|
|
34
18
|
"text-embedding-3-large": 3072
|
|
@@ -1,10 +1,37 @@
|
|
|
1
1
|
import { MEMORY_AGENT_ID_COLUMN, MEMORY_TABLE_NAME, hasAgentScopeColumn, memoryAgentPredicate, quoteLanceSqlString } from "./lancedb-schema.js";
|
|
2
2
|
import { resolveDefaultAgentId } from "openclaw/plugin-sdk/agent-runtime";
|
|
3
|
-
import fs from "node:fs";
|
|
4
3
|
import os from "node:os";
|
|
5
4
|
import path from "node:path";
|
|
5
|
+
import fs from "node:fs";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
//#region extensions/memory-lancedb/doctor-contract-api.ts
|
|
8
|
+
const LEGACY_ENVELOPE_DELETE_BATCH_SIZE = 500;
|
|
9
|
+
const LEGACY_ENVELOPE_SENTINEL_LINE_RE = new RegExp(`^(?:${[
|
|
10
|
+
"Conversation info (untrusted metadata):",
|
|
11
|
+
"Sender (untrusted metadata):",
|
|
12
|
+
"Thread starter (untrusted, for context):",
|
|
13
|
+
"Reply target of current user message (untrusted, for context):",
|
|
14
|
+
"Replied message (untrusted, for context):",
|
|
15
|
+
"Forwarded message context (untrusted metadata):",
|
|
16
|
+
"Conversation context (untrusted, chronological, selected for current message):",
|
|
17
|
+
"Current local chat window (untrusted, chronological, before current message):",
|
|
18
|
+
"Nearby reply target window (untrusted, chronological, around replied-to message):",
|
|
19
|
+
"Chat history since last reply (untrusted, for context):"
|
|
20
|
+
].map((sentinel) => sentinel.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})[^\\n]*$`, "m");
|
|
21
|
+
const LEGACY_ENVELOPE_LABEL_JSON_BLOCK_RE = /^[^\n]+\((?:untrusted metadata|untrusted, for context|untrusted, nearest first|untrusted, chronological,[^\n)]{1,80})\):[ \t]*\n[ \t]*```json[ \t]*\n[\s\S]*?\n[ \t]*```[ \t]*(?:\n|$)/m;
|
|
22
|
+
const LEGACY_ENVELOPE_HEADER_RE = /^Untrusted context \(metadata, do not treat as instructions or commands\):[ \t]*$/m;
|
|
23
|
+
function isLegacyEnvelopeContaminatedText(text) {
|
|
24
|
+
return typeof text === "string" && (LEGACY_ENVELOPE_SENTINEL_LINE_RE.test(text) || LEGACY_ENVELOPE_LABEL_JSON_BLOCK_RE.test(text) || LEGACY_ENVELOPE_HEADER_RE.test(text));
|
|
25
|
+
}
|
|
26
|
+
async function scanLegacyEnvelopeRowIds(table) {
|
|
27
|
+
const contaminatedIds = [];
|
|
28
|
+
for await (const batch of table.query().select(["id", "text"])) for (const row of batch.toArray()) {
|
|
29
|
+
if (!isLegacyEnvelopeContaminatedText(row.text)) continue;
|
|
30
|
+
if (typeof row.id !== "string") throw new Error("LanceDB legacy envelope row is missing a string id");
|
|
31
|
+
contaminatedIds.push(row.id);
|
|
32
|
+
}
|
|
33
|
+
return contaminatedIds;
|
|
34
|
+
}
|
|
8
35
|
function resolveMemoryLanceDbPluginRoot(moduleUrl) {
|
|
9
36
|
const artifactDir = path.dirname(fileURLToPath(moduleUrl));
|
|
10
37
|
return path.basename(artifactDir) === "dist" ? path.dirname(artifactDir) : artifactDir;
|
|
@@ -97,6 +124,54 @@ function createMemoryLanceDbStateMigrations(pluginRoot = DEFAULT_PLUGIN_ROOT) {
|
|
|
97
124
|
opened.connection?.close();
|
|
98
125
|
}
|
|
99
126
|
}
|
|
127
|
+
}, {
|
|
128
|
+
id: "memory-lancedb-legacy-envelope-rows",
|
|
129
|
+
label: "Memory LanceDB legacy envelope contamination",
|
|
130
|
+
doctorOnly: true,
|
|
131
|
+
async detectLegacyState(params) {
|
|
132
|
+
const opened = await openMemoryTable({
|
|
133
|
+
...params,
|
|
134
|
+
pluginRoot
|
|
135
|
+
});
|
|
136
|
+
try {
|
|
137
|
+
if (!opened.table) return null;
|
|
138
|
+
const contaminatedIds = await scanLegacyEnvelopeRowIds(opened.table);
|
|
139
|
+
if (contaminatedIds.length === 0) return null;
|
|
140
|
+
return { preview: [`- Memory LanceDB: delete ${contaminatedIds.length} memory ${contaminatedIds.length === 1 ? "row" : "rows"} contaminated with legacy envelope metadata at ${opened.dbPath}`] };
|
|
141
|
+
} finally {
|
|
142
|
+
opened.table?.close();
|
|
143
|
+
opened.connection?.close();
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
async migrateLegacyState(params) {
|
|
147
|
+
const opened = await openMemoryTable({
|
|
148
|
+
...params,
|
|
149
|
+
pluginRoot
|
|
150
|
+
});
|
|
151
|
+
try {
|
|
152
|
+
if (!opened.table) return {
|
|
153
|
+
changes: [],
|
|
154
|
+
warnings: []
|
|
155
|
+
};
|
|
156
|
+
const contaminatedIds = await scanLegacyEnvelopeRowIds(opened.table);
|
|
157
|
+
if (contaminatedIds.length === 0) return {
|
|
158
|
+
changes: [],
|
|
159
|
+
warnings: []
|
|
160
|
+
};
|
|
161
|
+
for (let offset = 0; offset < contaminatedIds.length; offset += LEGACY_ENVELOPE_DELETE_BATCH_SIZE) {
|
|
162
|
+
const batch = contaminatedIds.slice(offset, offset + LEGACY_ENVELOPE_DELETE_BATCH_SIZE);
|
|
163
|
+
await opened.table.delete(`id IN (${batch.map((id) => quoteLanceSqlString(id)).join(", ")})`);
|
|
164
|
+
}
|
|
165
|
+
if ((await scanLegacyEnvelopeRowIds(opened.table)).length !== 0) throw new Error("LanceDB legacy envelope row migration verification failed");
|
|
166
|
+
return {
|
|
167
|
+
changes: [`Deleted ${contaminatedIds.length} Memory LanceDB ${contaminatedIds.length === 1 ? "row" : "rows"} contaminated with legacy envelope metadata`],
|
|
168
|
+
warnings: []
|
|
169
|
+
};
|
|
170
|
+
} finally {
|
|
171
|
+
opened.table?.close();
|
|
172
|
+
opened.connection?.close();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
100
175
|
}];
|
|
101
176
|
}
|
|
102
177
|
const stateMigrations = createMemoryLanceDbStateMigrations();
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
|
|
2
|
+
import { asOptionalRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
3
|
+
import { resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
|
|
4
|
+
import { Buffer } from "node:buffer";
|
|
5
|
+
import { toErrorObject } from "openclaw/plugin-sdk/error-runtime";
|
|
6
|
+
import { resolveGlobalSingleton } from "openclaw/plugin-sdk/global-singleton";
|
|
7
|
+
import { canonicalizeBase64 } from "openclaw/plugin-sdk/media-runtime";
|
|
8
|
+
import { ensureGlobalUndiciEnvProxyDispatcher } from "openclaw/plugin-sdk/runtime-env";
|
|
9
|
+
//#region extensions/memory-lancedb/embeddings.ts
|
|
10
|
+
const loadOpenAiModule = createLazyRuntimeModule(() => import("openai"));
|
|
11
|
+
const loadMemoryEmbeddingProviderModule = createLazyRuntimeModule(() => import("openclaw/plugin-sdk/memory-core-host-engine-embeddings"));
|
|
12
|
+
const loadMemoryHostCoreModule = createLazyRuntimeModule(() => import("openclaw/plugin-sdk/memory-host-core"));
|
|
13
|
+
const PROVIDER_ADAPTER_LIFECYCLE = resolveGlobalSingleton(Symbol.for("openclaw.memoryLanceDbEmbeddingProviderLifecycle.v1"), () => ({
|
|
14
|
+
retainedProviders: /* @__PURE__ */ new Set(),
|
|
15
|
+
tail: Promise.resolve()
|
|
16
|
+
}));
|
|
17
|
+
function runProviderAdapterLifecycle(operation) {
|
|
18
|
+
const result = PROVIDER_ADAPTER_LIFECYCLE.tail.then(operation, operation);
|
|
19
|
+
PROVIDER_ADAPTER_LIFECYCLE.tail = result.then(() => void 0, () => void 0);
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
async function drainRetainedProviders() {
|
|
23
|
+
let firstError;
|
|
24
|
+
let closeFailed = false;
|
|
25
|
+
for (const provider of PROVIDER_ADAPTER_LIFECYCLE.retainedProviders) try {
|
|
26
|
+
await provider.close?.();
|
|
27
|
+
PROVIDER_ADAPTER_LIFECYCLE.retainedProviders.delete(provider);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
if (!closeFailed) firstError = err;
|
|
30
|
+
closeFailed = true;
|
|
31
|
+
}
|
|
32
|
+
if (closeFailed) throw toErrorObject(firstError, "memory-lancedb embedding provider retirement failed");
|
|
33
|
+
}
|
|
34
|
+
var OpenAiCompatibleEmbeddings = class {
|
|
35
|
+
constructor(apiKey, model, baseUrl, dimensions) {
|
|
36
|
+
this.model = model;
|
|
37
|
+
this.dimensions = dimensions;
|
|
38
|
+
this.clientPromise = loadOpenAiModule().then(({ default: OpenAI }) => new OpenAI({
|
|
39
|
+
apiKey,
|
|
40
|
+
baseURL: baseUrl
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
async embed(text, options) {
|
|
44
|
+
const dimensions = this.dimensions;
|
|
45
|
+
const startedAtMs = options?.timeoutMs && Number.isFinite(options.timeoutMs) ? Date.now() : null;
|
|
46
|
+
try {
|
|
47
|
+
return normalizeEmbeddingVector((await this.postEmbedding(text, {
|
|
48
|
+
includeDimensions: true,
|
|
49
|
+
options
|
|
50
|
+
})).data?.[0]?.embedding);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (typeof dimensions !== "number" || !isEmbeddingDimensionsRejectedError(error)) throw error;
|
|
53
|
+
}
|
|
54
|
+
const fallbackOptions = startedAtMs === null || options?.timeoutMs === void 0 ? options : { timeoutMs: Math.max(1, options.timeoutMs - (Date.now() - startedAtMs)) };
|
|
55
|
+
return truncateEmbeddingVector(normalizeEmbeddingVector((await this.postEmbedding(text, {
|
|
56
|
+
includeDimensions: false,
|
|
57
|
+
options: fallbackOptions
|
|
58
|
+
})).data?.[0]?.embedding), dimensions, this.model);
|
|
59
|
+
}
|
|
60
|
+
async postEmbedding(text, request) {
|
|
61
|
+
const params = {
|
|
62
|
+
model: this.model,
|
|
63
|
+
input: text,
|
|
64
|
+
...request.includeDimensions && typeof this.dimensions === "number" ? { dimensions: this.dimensions } : {}
|
|
65
|
+
};
|
|
66
|
+
ensureGlobalUndiciEnvProxyDispatcher();
|
|
67
|
+
return await (await this.clientPromise).post("/embeddings", {
|
|
68
|
+
body: params,
|
|
69
|
+
...request.options?.timeoutMs ? {
|
|
70
|
+
timeout: request.options.timeoutMs,
|
|
71
|
+
maxRetries: 0
|
|
72
|
+
} : {}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
function isEmbeddingDimensionsRejectedError(error) {
|
|
77
|
+
const record = asOptionalRecord(error);
|
|
78
|
+
if (record?.status !== 400 && record?.status !== 422) return false;
|
|
79
|
+
const details = stringifyEmbeddingApiError(error).toLowerCase();
|
|
80
|
+
return /\bdimensions\b/.test(details) && isUnsupportedEmbeddingFieldError(details);
|
|
81
|
+
}
|
|
82
|
+
function isUnsupportedEmbeddingFieldError(details) {
|
|
83
|
+
if (/\b(?:parameter|field|argument)[_ -]value\b/.test(details)) return false;
|
|
84
|
+
return /\bextra[_ -]forbidden\b/.test(details) || /\bextra inputs? (?:are )?not permitted\b/.test(details) || /\bextra fields? (?:are )?not permitted\b/.test(details) || /\b(?:unknown|unrecognized|unexpected|unsupported)[_ -](?:request[_ -])?(?:parameter|field|argument)\b/.test(details);
|
|
85
|
+
}
|
|
86
|
+
function stringifyEmbeddingApiError(error) {
|
|
87
|
+
const record = asOptionalRecord(error);
|
|
88
|
+
const parts = error instanceof Error ? [error.message] : [];
|
|
89
|
+
for (const value of [
|
|
90
|
+
record?.code,
|
|
91
|
+
record?.type,
|
|
92
|
+
record?.param,
|
|
93
|
+
record?.error
|
|
94
|
+
]) {
|
|
95
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
96
|
+
parts.push(String(value));
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (value && typeof value === "object") try {
|
|
100
|
+
parts.push(JSON.stringify(value));
|
|
101
|
+
} catch {}
|
|
102
|
+
}
|
|
103
|
+
return parts.join("\n");
|
|
104
|
+
}
|
|
105
|
+
function truncateEmbeddingVector(embedding, dimensions, model) {
|
|
106
|
+
if (embedding.length < dimensions) throw new Error(`Embedding model ${model} returned ${embedding.length} dimensions, need at least ${dimensions} for local truncation`);
|
|
107
|
+
const truncated = embedding.slice(0, dimensions);
|
|
108
|
+
const magnitude = Math.sqrt(truncated.reduce((sum, value) => sum + value * value, 0));
|
|
109
|
+
return magnitude > 0 ? truncated.map((value) => value / magnitude) : truncated;
|
|
110
|
+
}
|
|
111
|
+
var ProviderAdapterEmbeddings = class {
|
|
112
|
+
constructor(api, embedding) {
|
|
113
|
+
this.api = api;
|
|
114
|
+
this.embedding = embedding;
|
|
115
|
+
this.closePromise = null;
|
|
116
|
+
this.closed = false;
|
|
117
|
+
this.activeUses = 0;
|
|
118
|
+
this.idleWaiters = /* @__PURE__ */ new Set();
|
|
119
|
+
}
|
|
120
|
+
getProvider() {
|
|
121
|
+
this.providerPromise ??= this.createProvider().catch((err) => {
|
|
122
|
+
this.providerPromise = void 0;
|
|
123
|
+
throw err;
|
|
124
|
+
});
|
|
125
|
+
return this.providerPromise;
|
|
126
|
+
}
|
|
127
|
+
acquireUse() {
|
|
128
|
+
if (this.closed) throw new Error("memory-lancedb embeddings are closed");
|
|
129
|
+
this.activeUses += 1;
|
|
130
|
+
let released = false;
|
|
131
|
+
return () => {
|
|
132
|
+
if (released) return;
|
|
133
|
+
released = true;
|
|
134
|
+
this.activeUses -= 1;
|
|
135
|
+
if (this.activeUses === 0) {
|
|
136
|
+
const waiters = Array.from(this.idleWaiters);
|
|
137
|
+
this.idleWaiters.clear();
|
|
138
|
+
for (const resolve of waiters) resolve();
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
async awaitIdle() {
|
|
143
|
+
if (this.activeUses === 0) return;
|
|
144
|
+
await new Promise((resolve) => {
|
|
145
|
+
this.idleWaiters.add(resolve);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
async createProvider() {
|
|
149
|
+
return await runProviderAdapterLifecycle(async () => {
|
|
150
|
+
await drainRetainedProviders();
|
|
151
|
+
return await this.createProviderAfterRetirement();
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
async createProviderAfterRetirement() {
|
|
155
|
+
const cfg = this.api.runtime.config?.current?.() ?? this.api.config;
|
|
156
|
+
const providerId = this.embedding.provider;
|
|
157
|
+
const { getMemoryEmbeddingProvider } = await loadMemoryEmbeddingProviderModule();
|
|
158
|
+
const adapter = getMemoryEmbeddingProvider(providerId, cfg);
|
|
159
|
+
if (!adapter) throw new Error(`Unknown memory embedding provider: ${providerId}`);
|
|
160
|
+
const { resolveDefaultAgentId } = await loadMemoryHostCoreModule();
|
|
161
|
+
const defaultAgentId = resolveDefaultAgentId(cfg);
|
|
162
|
+
const agentDir = this.api.runtime.agent.resolveAgentDir(cfg, defaultAgentId);
|
|
163
|
+
const remote = this.embedding.apiKey || this.embedding.baseUrl ? {
|
|
164
|
+
...this.embedding.apiKey ? { apiKey: this.embedding.apiKey } : {},
|
|
165
|
+
...this.embedding.baseUrl ? { baseUrl: this.embedding.baseUrl } : {}
|
|
166
|
+
} : void 0;
|
|
167
|
+
const result = await adapter.create({
|
|
168
|
+
config: cfg,
|
|
169
|
+
agentDir,
|
|
170
|
+
provider: providerId,
|
|
171
|
+
fallback: "none",
|
|
172
|
+
model: this.embedding.model,
|
|
173
|
+
...remote ? { remote } : {},
|
|
174
|
+
...typeof this.embedding.dimensions === "number" ? { outputDimensionality: this.embedding.dimensions } : {}
|
|
175
|
+
});
|
|
176
|
+
if (!result.provider) throw new Error(`Memory embedding provider ${providerId} is unavailable.`);
|
|
177
|
+
return result.provider;
|
|
178
|
+
}
|
|
179
|
+
async embed(text, options) {
|
|
180
|
+
const releaseUse = this.acquireUse();
|
|
181
|
+
try {
|
|
182
|
+
const provider = await this.getProvider();
|
|
183
|
+
if (!options?.timeoutMs) return await provider.embedQuery(text);
|
|
184
|
+
const controller = new AbortController();
|
|
185
|
+
let timer;
|
|
186
|
+
try {
|
|
187
|
+
timer = setTimeout(() => controller.abort(/* @__PURE__ */ new Error("memory-lancedb embedding timed out")), resolveTimerTimeoutMs(options.timeoutMs, 1));
|
|
188
|
+
timer.unref?.();
|
|
189
|
+
return await provider.embedQuery(text, { signal: controller.signal });
|
|
190
|
+
} finally {
|
|
191
|
+
if (timer) clearTimeout(timer);
|
|
192
|
+
}
|
|
193
|
+
} finally {
|
|
194
|
+
releaseUse();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async close() {
|
|
198
|
+
const existingClose = this.closePromise;
|
|
199
|
+
if (existingClose) {
|
|
200
|
+
await existingClose;
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const closeOperation = this.closeOnce();
|
|
204
|
+
this.closePromise = closeOperation;
|
|
205
|
+
try {
|
|
206
|
+
await closeOperation;
|
|
207
|
+
} catch (err) {
|
|
208
|
+
if (this.closePromise === closeOperation) this.closePromise = null;
|
|
209
|
+
throw err;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async closeOnce() {
|
|
213
|
+
this.closed = true;
|
|
214
|
+
const providerPromise = this.providerPromise;
|
|
215
|
+
await runProviderAdapterLifecycle(async () => {
|
|
216
|
+
await this.awaitIdle();
|
|
217
|
+
const provider = await providerPromise?.catch(() => null);
|
|
218
|
+
if (provider) PROVIDER_ADAPTER_LIFECYCLE.retainedProviders.add(provider);
|
|
219
|
+
try {
|
|
220
|
+
await drainRetainedProviders();
|
|
221
|
+
} finally {
|
|
222
|
+
if (this.providerPromise === providerPromise) this.providerPromise = void 0;
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
async function runWithTimeout(params) {
|
|
228
|
+
let timeout;
|
|
229
|
+
const TIMEOUT = Symbol("timeout");
|
|
230
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
231
|
+
timeout = setTimeout(() => resolve(TIMEOUT), resolveTimerTimeoutMs(params.timeoutMs, 1));
|
|
232
|
+
timeout.unref?.();
|
|
233
|
+
});
|
|
234
|
+
const taskPromise = params.task();
|
|
235
|
+
taskPromise.catch(() => void 0);
|
|
236
|
+
try {
|
|
237
|
+
const result = await Promise.race([taskPromise, timeoutPromise]);
|
|
238
|
+
if (result === TIMEOUT) return { status: "timeout" };
|
|
239
|
+
return {
|
|
240
|
+
status: "ok",
|
|
241
|
+
value: result
|
|
242
|
+
};
|
|
243
|
+
} finally {
|
|
244
|
+
if (timeout) clearTimeout(timeout);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function formatMemoryRecallError(error) {
|
|
248
|
+
return error instanceof Error ? error.message : String(error);
|
|
249
|
+
}
|
|
250
|
+
function isMemoryRecallTimeoutError(error) {
|
|
251
|
+
let current = error;
|
|
252
|
+
for (let depth = 0; depth < 3 && current !== void 0; depth += 1) {
|
|
253
|
+
const record = asOptionalRecord(current);
|
|
254
|
+
const name = current instanceof Error ? current.name : typeof record?.name === "string" ? record.name : "";
|
|
255
|
+
const message = current instanceof Error ? current.message : typeof record?.message === "string" ? record.message : "";
|
|
256
|
+
const code = typeof record?.code === "string" ? record.code : "";
|
|
257
|
+
if (name === "APIConnectionTimeoutError" || name === "TimeoutError" || code === "ETIMEDOUT" || /^UND_ERR_.*_TIMEOUT$/.test(code) || /\btimed out\b/i.test(message)) return true;
|
|
258
|
+
current = record?.cause;
|
|
259
|
+
}
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
function buildMemoryRecallUnavailableResult(error) {
|
|
263
|
+
return {
|
|
264
|
+
content: [{
|
|
265
|
+
type: "text",
|
|
266
|
+
text: "Memory recall is unavailable right now."
|
|
267
|
+
}],
|
|
268
|
+
details: {
|
|
269
|
+
count: 0,
|
|
270
|
+
disabled: true,
|
|
271
|
+
unavailable: true,
|
|
272
|
+
error
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
var MemoryRecallEmbeddingError = class extends Error {
|
|
277
|
+
constructor(originalError) {
|
|
278
|
+
super(formatMemoryRecallError(originalError));
|
|
279
|
+
this.originalError = originalError;
|
|
280
|
+
this.name = "MemoryRecallEmbeddingError";
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
const testing = {
|
|
284
|
+
isEmbeddingDimensionsRejectedError,
|
|
285
|
+
isMemoryRecallTimeoutError,
|
|
286
|
+
runWithTimeout,
|
|
287
|
+
truncateEmbeddingVector
|
|
288
|
+
};
|
|
289
|
+
function createEmbeddings(api, cfg) {
|
|
290
|
+
const { provider, model, dimensions, apiKey, baseUrl } = cfg.embedding;
|
|
291
|
+
if (provider === "openai" && apiKey) return new OpenAiCompatibleEmbeddings(apiKey, model, baseUrl, dimensions);
|
|
292
|
+
return new ProviderAdapterEmbeddings(api, cfg.embedding);
|
|
293
|
+
}
|
|
294
|
+
function normalizeEmbeddingVector(value) {
|
|
295
|
+
if (Array.isArray(value)) {
|
|
296
|
+
if (!value.every((item) => typeof item === "number" && Number.isFinite(item))) throw new Error("Embedding response contains non-numeric values");
|
|
297
|
+
return value;
|
|
298
|
+
}
|
|
299
|
+
if (typeof value === "string") {
|
|
300
|
+
const canonicalEmbedding = canonicalizeBase64(value);
|
|
301
|
+
if (!canonicalEmbedding) throw new Error("Base64 embedding response is malformed");
|
|
302
|
+
const bytes = Buffer.from(canonicalEmbedding, "base64");
|
|
303
|
+
if (bytes.byteLength % Float32Array.BYTES_PER_ELEMENT !== 0) throw new Error("Base64 embedding response has invalid byte length");
|
|
304
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
305
|
+
const floats = [];
|
|
306
|
+
for (let offset = 0; offset < bytes.byteLength; offset += Float32Array.BYTES_PER_ELEMENT) floats.push(view.getFloat32(offset, true));
|
|
307
|
+
return floats;
|
|
308
|
+
}
|
|
309
|
+
throw new Error("Embedding response is missing a vector");
|
|
310
|
+
}
|
|
311
|
+
//#endregion
|
|
312
|
+
export { MemoryRecallEmbeddingError, buildMemoryRecallUnavailableResult, createEmbeddings, formatMemoryRecallError, isMemoryRecallTimeoutError, normalizeEmbeddingVector, runWithTimeout, testing };
|