@xdarkicex/openclaw-memory-libravdb 1.3.5

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.
Files changed (80) hide show
  1. package/README.md +46 -0
  2. package/docs/README.md +14 -0
  3. package/docs/architecture-decisions/README.md +6 -0
  4. package/docs/architecture-decisions/adr-001-onnx-over-ollama.md +21 -0
  5. package/docs/architecture-decisions/adr-002-libravdb-over-lancedb.md +19 -0
  6. package/docs/architecture-decisions/adr-003-convex-gating-over-threshold.md +27 -0
  7. package/docs/architecture-decisions/adr-004-sidecar-over-native-ts.md +21 -0
  8. package/docs/architecture.md +188 -0
  9. package/docs/contributing.md +76 -0
  10. package/docs/dependencies.md +38 -0
  11. package/docs/embedding-profiles.md +42 -0
  12. package/docs/gating.md +329 -0
  13. package/docs/implementation.md +381 -0
  14. package/docs/installation.md +272 -0
  15. package/docs/mathematics.md +695 -0
  16. package/docs/models.md +63 -0
  17. package/docs/problem.md +64 -0
  18. package/docs/security.md +86 -0
  19. package/openclaw.plugin.json +84 -0
  20. package/package.json +41 -0
  21. package/scripts/build-sidecar.sh +30 -0
  22. package/scripts/postinstall.js +169 -0
  23. package/scripts/setup.sh +20 -0
  24. package/scripts/setup.ts +505 -0
  25. package/scripts/sidecar-release.d.ts +4 -0
  26. package/scripts/sidecar-release.js +17 -0
  27. package/sidecar/cmd/inspect_onnx/main.go +105 -0
  28. package/sidecar/compact/gate.go +273 -0
  29. package/sidecar/compact/gate_test.go +85 -0
  30. package/sidecar/compact/summarize.go +345 -0
  31. package/sidecar/compact/summarize_test.go +319 -0
  32. package/sidecar/compact/tokens.go +11 -0
  33. package/sidecar/config/config.go +119 -0
  34. package/sidecar/config/config_test.go +75 -0
  35. package/sidecar/embed/engine.go +696 -0
  36. package/sidecar/embed/engine_test.go +349 -0
  37. package/sidecar/embed/matryoshka.go +93 -0
  38. package/sidecar/embed/matryoshka_test.go +150 -0
  39. package/sidecar/embed/onnx_local.go +319 -0
  40. package/sidecar/embed/onnx_local_test.go +159 -0
  41. package/sidecar/embed/profile_contract_test.go +71 -0
  42. package/sidecar/embed/profile_eval_test.go +923 -0
  43. package/sidecar/embed/profiles.go +39 -0
  44. package/sidecar/go.mod +21 -0
  45. package/sidecar/go.sum +30 -0
  46. package/sidecar/health/check.go +33 -0
  47. package/sidecar/health/check_test.go +55 -0
  48. package/sidecar/main.go +151 -0
  49. package/sidecar/model/encoder.go +222 -0
  50. package/sidecar/model/registry.go +262 -0
  51. package/sidecar/model/registry_test.go +102 -0
  52. package/sidecar/model/seq2seq.go +133 -0
  53. package/sidecar/server/rpc.go +343 -0
  54. package/sidecar/server/rpc_test.go +350 -0
  55. package/sidecar/server/transport.go +160 -0
  56. package/sidecar/store/libravdb.go +676 -0
  57. package/sidecar/store/libravdb_test.go +472 -0
  58. package/sidecar/summarize/engine.go +360 -0
  59. package/sidecar/summarize/engine_test.go +148 -0
  60. package/sidecar/summarize/onnx_local.go +494 -0
  61. package/sidecar/summarize/onnx_local_test.go +48 -0
  62. package/sidecar/summarize/profiles.go +52 -0
  63. package/sidecar/summarize/tokenizer.go +13 -0
  64. package/sidecar/summarize/tokenizer_hf.go +76 -0
  65. package/sidecar/summarize/util.go +13 -0
  66. package/src/cli.ts +205 -0
  67. package/src/context-engine.ts +195 -0
  68. package/src/index.ts +27 -0
  69. package/src/memory-provider.ts +24 -0
  70. package/src/openclaw-plugin-sdk.d.ts +53 -0
  71. package/src/plugin-runtime.ts +67 -0
  72. package/src/recall-cache.ts +34 -0
  73. package/src/recall-utils.ts +22 -0
  74. package/src/rpc.ts +84 -0
  75. package/src/scoring.ts +58 -0
  76. package/src/sidecar.ts +506 -0
  77. package/src/tokens.ts +36 -0
  78. package/src/types.ts +146 -0
  79. package/tsconfig.json +20 -0
  80. package/tsconfig.tests.json +12 -0
package/src/rpc.ts ADDED
@@ -0,0 +1,84 @@
1
+ import type { RpcCallOptions, SidecarSocket } from "./types.js";
2
+
3
+ interface PendingCall {
4
+ resolve(value: unknown): void;
5
+ reject(error: Error): void;
6
+ timer: ReturnType<typeof setTimeout>;
7
+ }
8
+
9
+ export class RpcClient {
10
+ private seq = 0;
11
+ private readonly pending = new Map<number, PendingCall>();
12
+ private buf = "";
13
+
14
+ constructor(
15
+ private readonly socket: SidecarSocket,
16
+ private readonly options: RpcCallOptions,
17
+ ) {
18
+ socket.setEncoding("utf8");
19
+ socket.on("data", (chunk) => this.handleData(chunk));
20
+ socket.on("close", () => this.rejectAll(new Error("Socket closed")));
21
+ }
22
+
23
+ async call<T>(method: string, _params: unknown): Promise<T> {
24
+ return await new Promise<T>((resolve, reject) => {
25
+ const id = ++this.seq;
26
+ const timer = setTimeout(() => {
27
+ this.pending.delete(id);
28
+ reject(new Error(`RPC timeout: ${method} (${this.options.timeoutMs}ms)`));
29
+ }, this.options.timeoutMs);
30
+
31
+ this.pending.set(id, { resolve, reject, timer });
32
+ this.socket.write(
33
+ `${JSON.stringify({ jsonrpc: "2.0", id, method, params: _params })}\n`,
34
+ );
35
+ });
36
+ }
37
+
38
+ private handleData(chunk: string): void {
39
+ this.buf += chunk;
40
+ const lines = this.buf.split("\n");
41
+ this.buf = lines.pop() ?? "";
42
+
43
+ for (const line of lines) {
44
+ if (!line.trim()) {
45
+ continue;
46
+ }
47
+
48
+ try {
49
+ const msg = JSON.parse(line) as {
50
+ id?: number;
51
+ result?: unknown;
52
+ error?: { message?: string };
53
+ };
54
+ if (typeof msg.id !== "number") {
55
+ continue;
56
+ }
57
+
58
+ const pending = this.pending.get(msg.id);
59
+ if (!pending) {
60
+ continue;
61
+ }
62
+
63
+ clearTimeout(pending.timer);
64
+ this.pending.delete(msg.id);
65
+
66
+ if (msg.error?.message) {
67
+ pending.reject(new Error(msg.error.message));
68
+ } else {
69
+ pending.resolve(msg.result);
70
+ }
71
+ } catch {
72
+ // Ignore malformed frames and keep parsing future lines.
73
+ }
74
+ }
75
+ }
76
+
77
+ private rejectAll(error: Error): void {
78
+ for (const [id, pending] of this.pending.entries()) {
79
+ clearTimeout(pending.timer);
80
+ this.pending.delete(id);
81
+ pending.reject(error);
82
+ }
83
+ }
84
+ }
package/src/scoring.ts ADDED
@@ -0,0 +1,58 @@
1
+ import type { SearchResult } from "./types.js";
2
+
3
+ interface HybridOptions {
4
+ alpha?: number;
5
+ beta?: number;
6
+ gamma?: number;
7
+ delta?: number;
8
+ recencyLambdaSession?: number;
9
+ recencyLambdaUser?: number;
10
+ recencyLambdaGlobal?: number;
11
+ sessionId: string;
12
+ userId: string;
13
+ }
14
+
15
+ export function scoreCandidates(items: SearchResult[], opts: HybridOptions): SearchResult[] {
16
+ const now = Date.now();
17
+ const alpha = opts.alpha ?? 0.7;
18
+ const beta = opts.beta ?? 0.2;
19
+ const gamma = opts.gamma ?? 0.1;
20
+ const delta = opts.delta ?? 0.5;
21
+ // Lambda units are per-second decay constants.
22
+ const recencyLambdaSession = opts.recencyLambdaSession ?? 0.0001;
23
+ const recencyLambdaUser = opts.recencyLambdaUser ?? 0.00001;
24
+ const recencyLambdaGlobal = opts.recencyLambdaGlobal ?? 0.000002;
25
+
26
+ return items
27
+ .map((item) => {
28
+ const ts = typeof item.metadata.ts === "number" ? item.metadata.ts : now;
29
+ const lambda =
30
+ item.metadata.sessionId === opts.sessionId ? recencyLambdaSession
31
+ : item.metadata.userId === opts.userId ? recencyLambdaUser
32
+ : recencyLambdaGlobal;
33
+ const ageSeconds = Math.max(0, now - ts) / 1000;
34
+ const recency = Math.exp(-lambda * ageSeconds);
35
+ const scopeBoost =
36
+ item.metadata.sessionId === opts.sessionId ? 1.0
37
+ : item.metadata.userId === opts.userId ? 0.6
38
+ : 0.3;
39
+ const baseScore =
40
+ alpha * item.score +
41
+ beta * recency +
42
+ gamma * scopeBoost;
43
+ const rawDecayRate =
44
+ typeof item.metadata.decay_rate === "number" ? item.metadata.decay_rate : 0.0;
45
+ const decayRate = Math.min(1, Math.max(0, rawDecayRate));
46
+ const quality =
47
+ item.metadata.type === "summary"
48
+ ? 1.0 - delta * decayRate
49
+ : 1.0;
50
+ const finalScore = baseScore * quality;
51
+
52
+ return {
53
+ ...item,
54
+ finalScore,
55
+ };
56
+ })
57
+ .sort((a, b) => (b.finalScore ?? 0) - (a.finalScore ?? 0));
58
+ }
package/src/sidecar.ts ADDED
@@ -0,0 +1,506 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import net from "node:net";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ import type { LoggerLike, PluginConfig, SidecarHandle, SidecarSocket } from "./types.js";
8
+
9
+ type CloseHandler = () => void;
10
+ type DataHandler = (chunk: string) => void;
11
+ type ErrorHandler = (error: Error) => void;
12
+
13
+ export interface SidecarRuntime {
14
+ prepareLaunch?(cfg: PluginConfig, env: Record<string, string>): void | Promise<void>;
15
+ resolveEndpoint(cfg: PluginConfig): string | Promise<string>;
16
+ createSocket(endpoint: string): SidecarSocket;
17
+ scheduleRestart(delayMs: number, restart: () => void): void;
18
+ stop?(): void | Promise<void>;
19
+ }
20
+
21
+ type HostSignal = "exit" | "SIGINT" | "SIGTERM" | "SIGHUP";
22
+ type HostProcessLike = {
23
+ once(event: HostSignal, handler: () => void): void;
24
+ off?(event: HostSignal, handler: () => void): void;
25
+ };
26
+
27
+ class PlaceholderSocket implements SidecarSocket {
28
+ private readonly onData = new Set<DataHandler>();
29
+ private readonly onClose = new Set<CloseHandler>();
30
+ private readonly connectOnce = new Set<CloseHandler>();
31
+ private readonly errorOnce = new Set<ErrorHandler>();
32
+
33
+ constructor() {
34
+ queueMicrotask(() => {
35
+ for (const handler of this.connectOnce) {
36
+ handler();
37
+ }
38
+ this.connectOnce.clear();
39
+ });
40
+ }
41
+
42
+ setEncoding(_encoding: string): void {}
43
+
44
+ on(event: "data" | "close", handler: DataHandler | CloseHandler): void {
45
+ if (event === "data") {
46
+ this.onData.add(handler as DataHandler);
47
+ return;
48
+ }
49
+ this.onClose.add(handler as CloseHandler);
50
+ }
51
+
52
+ once(event: "connect" | "error", handler: CloseHandler | ErrorHandler): void {
53
+ if (event === "connect") {
54
+ this.connectOnce.add(handler as CloseHandler);
55
+ return;
56
+ }
57
+ this.errorOnce.add(handler as ErrorHandler);
58
+ }
59
+
60
+ write(chunk: string): void {
61
+ try {
62
+ const msg = JSON.parse(chunk);
63
+ const response = JSON.stringify({
64
+ jsonrpc: "2.0",
65
+ id: msg.id,
66
+ result: msg.method === "health" ? { ok: true } : {},
67
+ });
68
+ for (const handler of this.onData) {
69
+ handler(`${response}\n`);
70
+ }
71
+ } catch (error) {
72
+ const err = error instanceof Error ? error : new Error(String(error));
73
+ for (const handler of this.errorOnce) {
74
+ handler(err);
75
+ }
76
+ this.errorOnce.clear();
77
+ }
78
+ }
79
+
80
+ destroy(): void {
81
+ for (const handler of this.onClose) {
82
+ handler();
83
+ }
84
+ }
85
+ }
86
+
87
+ class SupervisorSocket implements SidecarSocket {
88
+ private readonly onData = new Set<DataHandler>();
89
+ private readonly onClose = new Set<CloseHandler>();
90
+ private readonly connectOnce = new Set<CloseHandler>();
91
+ private readonly errorOnce = new Set<ErrorHandler>();
92
+ private current?: SidecarSocket;
93
+ private encoding = "utf8";
94
+ private generation = 0;
95
+
96
+ bind(socket: SidecarSocket): void {
97
+ this.current = socket;
98
+ this.generation += 1;
99
+ const generation = this.generation;
100
+
101
+ socket.setEncoding(this.encoding);
102
+ socket.on("data", (chunk) => {
103
+ if (generation !== this.generation) {
104
+ return;
105
+ }
106
+ for (const handler of this.onData) {
107
+ handler(chunk);
108
+ }
109
+ });
110
+ socket.on("close", () => {
111
+ if (generation !== this.generation) {
112
+ return;
113
+ }
114
+ for (const handler of this.onClose) {
115
+ handler();
116
+ }
117
+ });
118
+
119
+ for (const handler of this.connectOnce) {
120
+ handler();
121
+ }
122
+ this.connectOnce.clear();
123
+ }
124
+
125
+ setEncoding(encoding: string): void {
126
+ this.encoding = encoding;
127
+ this.current?.setEncoding(encoding);
128
+ }
129
+
130
+ on(event: "data" | "close", handler: DataHandler | CloseHandler): void {
131
+ if (event === "data") {
132
+ this.onData.add(handler as DataHandler);
133
+ return;
134
+ }
135
+ this.onClose.add(handler as CloseHandler);
136
+ }
137
+
138
+ once(event: "connect" | "error", handler: CloseHandler | ErrorHandler): void {
139
+ if (event === "connect") {
140
+ if (this.current) {
141
+ (handler as CloseHandler)();
142
+ return;
143
+ }
144
+ this.connectOnce.add(handler as CloseHandler);
145
+ return;
146
+ }
147
+ this.errorOnce.add(handler as ErrorHandler);
148
+ }
149
+
150
+ write(chunk: string): void {
151
+ if (!this.current) {
152
+ throw new Error("Sidecar socket unavailable");
153
+ }
154
+ this.current.write(chunk);
155
+ }
156
+
157
+ destroy(): void {
158
+ this.current?.destroy();
159
+ }
160
+ }
161
+
162
+ class SidecarSupervisor implements SidecarHandle {
163
+ private retries = 0;
164
+ private degraded = false;
165
+ private shuttingDown = false;
166
+ public socket: SidecarSocket;
167
+
168
+ constructor(
169
+ private readonly cfg: PluginConfig,
170
+ private readonly logger: LoggerLike,
171
+ private readonly runtime: SidecarRuntime,
172
+ ) {
173
+ this.socket = new SupervisorSocket();
174
+ }
175
+
176
+ async start(): Promise<SidecarSocket> {
177
+ const endpoint = await this.spawnEndpoint();
178
+ const socket = await this.connectEndpoint(endpoint);
179
+ if (this.socket instanceof SupervisorSocket) {
180
+ this.socket.bind(socket);
181
+ } else {
182
+ this.socket = socket;
183
+ }
184
+ return socket;
185
+ }
186
+
187
+ isDegraded(): boolean {
188
+ return this.degraded;
189
+ }
190
+
191
+ async shutdown(): Promise<void> {
192
+ this.shuttingDown = true;
193
+ await this.runtime.stop?.();
194
+ this.socket.destroy();
195
+ }
196
+
197
+ private async spawnEndpoint(): Promise<string> {
198
+ const env = buildSidecarEnv(this.cfg);
199
+ await this.runtime.prepareLaunch?.(this.cfg, env);
200
+ return await this.runtime.resolveEndpoint(this.cfg);
201
+ }
202
+
203
+ private async connectEndpoint(endpoint: string): Promise<SidecarSocket> {
204
+ const socket = this.runtime.createSocket(endpoint);
205
+ socket.on("close", () => {
206
+ void this.handleExit(1);
207
+ });
208
+
209
+ if (isTcpEndpoint(endpoint)) {
210
+ this.logger.info?.(`[libravdb] using TCP endpoint ${endpoint}`);
211
+ }
212
+
213
+ return await new Promise<SidecarSocket>((resolve, reject) => {
214
+ socket.once("connect", () => resolve(socket));
215
+ socket.once("error", reject);
216
+ });
217
+ }
218
+
219
+ private async handleExit(code: number | null): Promise<void> {
220
+ if (this.shuttingDown) {
221
+ return;
222
+ }
223
+ if (code === 0) {
224
+ return;
225
+ }
226
+
227
+ const maxRetries = this.cfg.maxRetries ?? 3;
228
+ if (this.retries >= maxRetries) {
229
+ this.logger.error("[libravdb] sidecar retries exhausted; degraded mode");
230
+ this.degraded = true;
231
+ return;
232
+ }
233
+
234
+ const backoffMs = computeBackoffMs(this.retries);
235
+ this.retries += 1;
236
+ this.runtime.scheduleRestart(backoffMs, () => {
237
+ void this.start().catch((error) => {
238
+ const message = error instanceof Error ? error.message : String(error);
239
+ this.logger.error(`[libravdb] sidecar restart failed: ${message}`);
240
+ });
241
+ });
242
+ }
243
+ }
244
+
245
+ export async function startSidecar(
246
+ cfg: PluginConfig,
247
+ logger: LoggerLike = console,
248
+ runtime: SidecarRuntime = createDefaultRuntime(logger),
249
+ ): Promise<SidecarHandle> {
250
+ const supervisor = new SidecarSupervisor(cfg, logger, runtime);
251
+ await supervisor.start();
252
+ return supervisor;
253
+ }
254
+
255
+ export function computeBackoffMs(retries: number): number {
256
+ return Math.min(500 * Math.pow(2, retries), 16000);
257
+ }
258
+
259
+ export function isTcpEndpoint(endpoint: string): boolean {
260
+ return endpoint.startsWith("tcp:");
261
+ }
262
+
263
+ export function resolveEndpoint(cfg: PluginConfig): string {
264
+ const endpoint = isConfiguredEndpoint(cfg.sidecarPath)
265
+ ? (cfg.sidecarPath ?? "unix:/tmp/libravdb.sock")
266
+ : "unix:/tmp/libravdb.sock";
267
+
268
+ return endpoint.replace(/^unix:/, "");
269
+ }
270
+
271
+ export function buildSidecarEnv(cfg: PluginConfig): Record<string, string> {
272
+ const env: Record<string, string> = {};
273
+
274
+ if (cfg.dbPath) {
275
+ env.LIBRAVDB_DB_PATH = cfg.dbPath;
276
+ }
277
+ if (cfg.embeddingRuntimePath) {
278
+ env.LIBRAVDB_ONNX_RUNTIME = cfg.embeddingRuntimePath;
279
+ }
280
+ if (cfg.embeddingBackend) {
281
+ env.LIBRAVDB_EMBEDDING_BACKEND = cfg.embeddingBackend;
282
+ }
283
+ if (cfg.embeddingProfile) {
284
+ env.LIBRAVDB_EMBEDDING_PROFILE = cfg.embeddingProfile;
285
+ }
286
+ if (cfg.fallbackProfile) {
287
+ env.LIBRAVDB_FALLBACK_PROFILE = cfg.fallbackProfile;
288
+ }
289
+ if (cfg.embeddingModelPath) {
290
+ env.LIBRAVDB_EMBEDDING_MODEL = cfg.embeddingModelPath;
291
+ }
292
+ if (cfg.embeddingTokenizerPath) {
293
+ env.LIBRAVDB_EMBEDDING_TOKENIZER = cfg.embeddingTokenizerPath;
294
+ }
295
+ if (typeof cfg.embeddingDimensions === "number" && cfg.embeddingDimensions > 0) {
296
+ env.LIBRAVDB_EMBEDDING_DIMENSIONS = String(cfg.embeddingDimensions);
297
+ }
298
+ if (typeof cfg.embeddingNormalize === "boolean") {
299
+ env.LIBRAVDB_EMBEDDING_NORMALIZE = String(cfg.embeddingNormalize);
300
+ }
301
+ if (cfg.summarizerBackend) {
302
+ env.LIBRAVDB_SUMMARIZER_BACKEND = cfg.summarizerBackend;
303
+ }
304
+ if (cfg.summarizerProfile) {
305
+ env.LIBRAVDB_SUMMARIZER_PROFILE = cfg.summarizerProfile;
306
+ }
307
+ if (cfg.summarizerRuntimePath) {
308
+ env.LIBRAVDB_SUMMARIZER_RUNTIME = cfg.summarizerRuntimePath;
309
+ }
310
+ if (cfg.summarizerModelPath) {
311
+ env.LIBRAVDB_SUMMARIZER_MODEL_PATH = cfg.summarizerModelPath;
312
+ }
313
+ if (cfg.summarizerTokenizerPath) {
314
+ env.LIBRAVDB_SUMMARIZER_TOKENIZER = cfg.summarizerTokenizerPath;
315
+ }
316
+ if (cfg.summarizerModel) {
317
+ env.LIBRAVDB_SUMMARIZER_MODEL = cfg.summarizerModel;
318
+ }
319
+ if (cfg.summarizerEndpoint) {
320
+ env.LIBRAVDB_SUMMARIZER_ENDPOINT = cfg.summarizerEndpoint;
321
+ }
322
+ if (cfg.ollamaUrl && !env.LIBRAVDB_SUMMARIZER_ENDPOINT) {
323
+ env.LIBRAVDB_SUMMARIZER_ENDPOINT = cfg.ollamaUrl;
324
+ }
325
+ if (cfg.compactModel && !env.LIBRAVDB_SUMMARIZER_MODEL) {
326
+ env.LIBRAVDB_SUMMARIZER_MODEL = cfg.compactModel;
327
+ }
328
+ if (cfg.gatingWeights?.w1c != null) {
329
+ env.LIBRAVDB_GATING_W1C = String(cfg.gatingWeights.w1c);
330
+ }
331
+ if (cfg.gatingWeights?.w2c != null) {
332
+ env.LIBRAVDB_GATING_W2C = String(cfg.gatingWeights.w2c);
333
+ }
334
+ if (cfg.gatingWeights?.w3c != null) {
335
+ env.LIBRAVDB_GATING_W3C = String(cfg.gatingWeights.w3c);
336
+ }
337
+ if (cfg.gatingWeights?.w1t != null) {
338
+ env.LIBRAVDB_GATING_W1T = String(cfg.gatingWeights.w1t);
339
+ }
340
+ if (cfg.gatingWeights?.w2t != null) {
341
+ env.LIBRAVDB_GATING_W2T = String(cfg.gatingWeights.w2t);
342
+ }
343
+ if (cfg.gatingWeights?.w3t != null) {
344
+ env.LIBRAVDB_GATING_W3T = String(cfg.gatingWeights.w3t);
345
+ }
346
+ if (typeof cfg.gatingTechNorm === "number" && cfg.gatingTechNorm > 0) {
347
+ env.LIBRAVDB_GATING_TECH_NORM = String(cfg.gatingTechNorm);
348
+ }
349
+ if (typeof cfg.ingestionGateThreshold === "number" && cfg.ingestionGateThreshold >= 0) {
350
+ env.LIBRAVDB_GATING_THRESHOLD = String(cfg.ingestionGateThreshold);
351
+ }
352
+ if (typeof cfg.gatingCentroidK === "number" && cfg.gatingCentroidK > 0) {
353
+ env.LIBRAVDB_GATING_CENTROID_K = String(cfg.gatingCentroidK);
354
+ }
355
+
356
+ return env;
357
+ }
358
+
359
+ export function installSidecarProcessCleanup(host: HostProcessLike, stop: () => void): () => void {
360
+ const events: HostSignal[] = ["exit", "SIGINT", "SIGTERM", "SIGHUP"];
361
+ let stopped = false;
362
+ const stopOnce = () => {
363
+ if (stopped) {
364
+ return;
365
+ }
366
+ stopped = true;
367
+ stop();
368
+ };
369
+ for (const event of events) {
370
+ host.once(event, stopOnce);
371
+ }
372
+ return () => {
373
+ for (const event of events) {
374
+ host.off?.(event, stopOnce);
375
+ }
376
+ };
377
+ }
378
+
379
+ function createDefaultRuntime(logger: LoggerLike): SidecarRuntime {
380
+ let launchEnv: Record<string, string> = {};
381
+ let proc: ReturnType<typeof spawn> | null = null;
382
+ const stopProc = () => {
383
+ if (!proc || proc.killed) {
384
+ proc = null;
385
+ return;
386
+ }
387
+ proc.kill();
388
+ proc = null;
389
+ };
390
+ const removeCleanup = installSidecarProcessCleanup(process, stopProc);
391
+
392
+ return {
393
+ prepareLaunch(_cfg, env) {
394
+ launchEnv = { ...env };
395
+ },
396
+ async resolveEndpoint(cfg) {
397
+ if (isConfiguredEndpoint(cfg.sidecarPath)) {
398
+ return resolveEndpoint(cfg);
399
+ }
400
+
401
+ const binPath = resolveBinPath(cfg.sidecarPath);
402
+ return await new Promise<string>((resolve, reject) => {
403
+ stopProc();
404
+ const child = spawn(binPath, [], {
405
+ env: {
406
+ ...process.env,
407
+ ...launchEnv,
408
+ },
409
+ stdio: ["ignore", "pipe", "pipe"],
410
+ });
411
+ proc = child;
412
+
413
+ let stdoutBuffer = "";
414
+ let settled = false;
415
+
416
+ child.stdout.setEncoding("utf8");
417
+ child.stdout.on("data", (chunk: string) => {
418
+ if (settled) {
419
+ return;
420
+ }
421
+ stdoutBuffer += chunk;
422
+ const lines = stdoutBuffer.split("\n");
423
+ stdoutBuffer = lines.pop() ?? "";
424
+ const endpoint = lines.find((line) => line.trim().length > 0)?.trim();
425
+ if (!endpoint) {
426
+ return;
427
+ }
428
+ settled = true;
429
+ resolve(endpoint);
430
+ });
431
+
432
+ child.stderr.setEncoding("utf8");
433
+ child.stderr.on("data", (chunk: string) => {
434
+ if (cfg.logLevel === "debug") {
435
+ process.stderr.write(chunk);
436
+ } else {
437
+ logger.info?.(`[libravdb] sidecar: ${chunk.trim()}`);
438
+ }
439
+ });
440
+
441
+ child.once("error", (error) => {
442
+ if (settled) {
443
+ return;
444
+ }
445
+ settled = true;
446
+ reject(error);
447
+ });
448
+
449
+ child.once("exit", (code) => {
450
+ proc = null;
451
+ if (settled) {
452
+ return;
453
+ }
454
+ settled = true;
455
+ reject(new Error(`LibraVDB sidecar exited before advertising an endpoint (code ${code ?? "unknown"})`));
456
+ });
457
+ });
458
+ },
459
+ createSocket(endpoint) {
460
+ if (isTcpEndpoint(endpoint)) {
461
+ const address = endpoint.slice("tcp:".length);
462
+ const separator = address.lastIndexOf(":");
463
+ if (separator <= 0) {
464
+ throw new Error(`Invalid TCP sidecar endpoint: ${endpoint}`);
465
+ }
466
+ return net.connect({
467
+ host: address.slice(0, separator),
468
+ port: Number(address.slice(separator + 1)),
469
+ }) as unknown as SidecarSocket;
470
+ }
471
+ return net.connect(endpoint) as unknown as SidecarSocket;
472
+ },
473
+ scheduleRestart(delayMs, restart) {
474
+ setTimeout(restart, delayMs);
475
+ },
476
+ async stop() {
477
+ stopProc();
478
+ removeCleanup();
479
+ },
480
+ };
481
+ }
482
+
483
+ function isConfiguredEndpoint(value?: string): boolean {
484
+ return value?.startsWith("tcp:") === true || value?.startsWith("unix:") === true;
485
+ }
486
+
487
+ function resolveBinPath(sidecarPath?: string): string {
488
+ if (sidecarPath && sidecarPath !== "auto") {
489
+ return sidecarPath;
490
+ }
491
+
492
+ const binaryName = process.platform === "win32" ? "libravdb-sidecar.exe" : "libravdb-sidecar";
493
+ const baseDir = path.dirname(fileURLToPath(import.meta.url));
494
+ const candidates = [
495
+ path.resolve(baseDir, "../.sidecar-bin", binaryName),
496
+ path.resolve(baseDir, "../../.sidecar-bin", binaryName),
497
+ ];
498
+
499
+ for (const candidate of candidates) {
500
+ if (existsSync(candidate)) {
501
+ return candidate;
502
+ }
503
+ }
504
+
505
+ return candidates[0];
506
+ }
package/src/tokens.ts ADDED
@@ -0,0 +1,36 @@
1
+ import type { SearchResult } from "./types.js";
2
+
3
+ export function estimateTokens(text: string): number {
4
+ const charsPerToken = detectCharsPerToken(text);
5
+ return Math.ceil(text.length / charsPerToken);
6
+ }
7
+
8
+ export function fitPromptBudget(items: SearchResult[], budget: number): SearchResult[] {
9
+ const selected: SearchResult[] = [];
10
+ let used = 0;
11
+
12
+ for (const item of items) {
13
+ const cost = estimateTokens(item.text);
14
+ if (used + cost > budget) {
15
+ continue;
16
+ }
17
+ selected.push(item);
18
+ used += cost;
19
+ }
20
+
21
+ return selected;
22
+ }
23
+
24
+ export function countTokens(messages: Array<{ content: string }>): number {
25
+ return messages.reduce((sum, msg) => sum + estimateTokens(msg.content), 0);
26
+ }
27
+
28
+ function detectCharsPerToken(text: string): number {
29
+ if (/[一-龯ぁ-ゖァ-ヺ가-힣]/u.test(text)) {
30
+ return 1.6;
31
+ }
32
+ if (/[Ѐ-ӿ؀-ۿ֐-׿]/u.test(text)) {
33
+ return 2.5;
34
+ }
35
+ return 4.0;
36
+ }