@xdarkicex/openclaw-memory-libravdb 1.3.19 → 1.3.21
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/README.md +1 -1
- package/docs/README.md +1 -1
- package/docs/architecture.md +8 -14
- package/docs/implementation.md +2 -2
- package/docs/mathematics-v2.md +485 -0
- package/package.json +1 -1
- package/src/context-engine.ts +50 -7
- package/src/memory-provider.ts +19 -81
- package/src/openclaw-plugin-sdk.d.ts +6 -1
- package/src/scoring.ts +93 -1
- package/src/sidecar.ts +31 -1
- package/src/temporal.ts +385 -0
- package/src/tokens.ts +16 -0
- package/src/types.ts +9 -0
package/src/context-engine.ts
CHANGED
|
@@ -12,7 +12,9 @@ import {
|
|
|
12
12
|
rankSection7VariantCandidates,
|
|
13
13
|
} from "./scoring.js";
|
|
14
14
|
import { buildInjectedMemoryMessageContent, buildMemoryHeader, recentIds } from "./recall-utils.js";
|
|
15
|
-
import {
|
|
15
|
+
import { detectTemporalQuerySignal, rankTemporalRecoveryCandidates } from "./temporal.js";
|
|
16
|
+
import type { TemporalRecoveryRankingResult } from "./temporal.js";
|
|
17
|
+
import { countTokens, estimateTokens, fitPromptBudget, fitPromptBudgetFirstFit } from "./tokens.js";
|
|
16
18
|
import type { RpcGetter } from "./plugin-runtime.js";
|
|
17
19
|
import type {
|
|
18
20
|
ContextAssembleArgs,
|
|
@@ -57,6 +59,7 @@ export function buildContextEngineFactory(
|
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
return {
|
|
62
|
+
info: { id: "libravdb-memory" },
|
|
60
63
|
ownsCompaction: true,
|
|
61
64
|
async bootstrap({ sessionId, userId }: ContextBootstrapArgs) {
|
|
62
65
|
const rpc = await getRpc();
|
|
@@ -190,6 +193,7 @@ export function buildContextEngineFactory(
|
|
|
190
193
|
systemPromptAddition: "",
|
|
191
194
|
} satisfies ContextAssembleResult;
|
|
192
195
|
}
|
|
196
|
+
const temporalQuery = detectTemporalQuerySignal(queryText);
|
|
193
197
|
|
|
194
198
|
const excluded = recentIds(messages, 4);
|
|
195
199
|
const cached = recallCache.take({ userId, queryText });
|
|
@@ -253,6 +257,7 @@ export function buildContextEngineFactory(
|
|
|
253
257
|
cached,
|
|
254
258
|
excluded,
|
|
255
259
|
queryText,
|
|
260
|
+
temporalQuery,
|
|
256
261
|
sessionId,
|
|
257
262
|
userId,
|
|
258
263
|
messages,
|
|
@@ -287,6 +292,7 @@ export function buildContextEngineFactory(
|
|
|
287
292
|
cached,
|
|
288
293
|
excluded,
|
|
289
294
|
queryText,
|
|
295
|
+
temporalQuery,
|
|
290
296
|
sessionId,
|
|
291
297
|
userId,
|
|
292
298
|
messages,
|
|
@@ -303,6 +309,7 @@ export function buildContextEngineFactory(
|
|
|
303
309
|
cached: ReturnType<RecallCache<SearchResult>["take"]>;
|
|
304
310
|
excluded: string[];
|
|
305
311
|
queryText: string;
|
|
312
|
+
temporalQuery: ReturnType<typeof detectTemporalQuerySignal>;
|
|
306
313
|
sessionId: string;
|
|
307
314
|
userId: string;
|
|
308
315
|
messages: Array<{ role: string; content: string }>;
|
|
@@ -562,6 +569,7 @@ export function buildContextEngineFactory(
|
|
|
562
569
|
// it never modifies the C_total(q) output and does not spend from tau_V.
|
|
563
570
|
let recoveryItems: SearchResult[] = [];
|
|
564
571
|
let rawUserRecoveryDebug: NonNullable<NonNullable<ContextAssembleResult["_debug"]>["rawUserRecoveryCandidates"]> = [];
|
|
572
|
+
let temporalRecoveryResult: TemporalRecoveryRankingResult | null = null;
|
|
565
573
|
if (recoveryTrigger.fire || crossSessionRawRecovery) {
|
|
566
574
|
profiler?.mark("recovery_expand");
|
|
567
575
|
const recoveryExcludeIDs = [...excluded, ...recentTailIDs, ...theoremSelectedIDs];
|
|
@@ -599,14 +607,44 @@ export function buildContextEngineFactory(
|
|
|
599
607
|
k: Math.max((cfg.topK ?? 8) * 4, 8),
|
|
600
608
|
excludeIds: recoveryExcludeIDs,
|
|
601
609
|
});
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
610
|
+
const annotatedUserResults = annotateCollection(rawUserResults.results ?? [], `turns:${userId}`);
|
|
611
|
+
temporalRecoveryResult = temporalQuery.active
|
|
612
|
+
? rankTemporalRecoveryCandidates(annotatedUserResults, {
|
|
613
|
+
queryText,
|
|
614
|
+
maxSelected: 3,
|
|
615
|
+
nowMs: Date.now(),
|
|
616
|
+
recencyLambda: cfg.recencyLambdaUser ?? 0.00001,
|
|
617
|
+
})
|
|
618
|
+
: null;
|
|
619
|
+
const reranked = temporalRecoveryResult
|
|
620
|
+
? temporalRecoveryResult
|
|
621
|
+
: rankRawUserRecoveryCandidates(annotatedUserResults, { queryText });
|
|
606
622
|
if (debugRecovery) {
|
|
607
623
|
rawUserRecoveryDebug = reranked.debug.slice(0, 8).map((item) => ({
|
|
608
|
-
|
|
624
|
+
id: item.id,
|
|
625
|
+
text: item.text,
|
|
609
626
|
selected: false,
|
|
627
|
+
tokenEstimate: estimateTokens(item.text),
|
|
628
|
+
temporalAnchorDensity: "temporalAnchorDensity" in item && typeof item.temporalAnchorDensity === "number"
|
|
629
|
+
? item.temporalAnchorDensity
|
|
630
|
+
: 0,
|
|
631
|
+
semanticScore: "semanticScore" in item && typeof item.semanticScore === "number"
|
|
632
|
+
? item.semanticScore
|
|
633
|
+
: 0,
|
|
634
|
+
slotCoverage: "slotCoverage" in item && typeof item.slotCoverage === "number"
|
|
635
|
+
? item.slotCoverage
|
|
636
|
+
: undefined,
|
|
637
|
+
slotMatches: "slotMatches" in item && Array.isArray(item.slotMatches)
|
|
638
|
+
? item.slotMatches
|
|
639
|
+
: undefined,
|
|
640
|
+
lexicalCoverage: "lexicalCoverage" in item && typeof item.lexicalCoverage === "number"
|
|
641
|
+
? item.lexicalCoverage
|
|
642
|
+
: ("slotCoverage" in item && typeof item.slotCoverage === "number" ? item.slotCoverage : 0),
|
|
643
|
+
recencyScore: "recencyScore" in item && typeof item.recencyScore === "number"
|
|
644
|
+
? item.recencyScore
|
|
645
|
+
: 0,
|
|
646
|
+
finalScore: typeof item.finalScore === "number" ? item.finalScore : 0,
|
|
647
|
+
rationale: typeof item.rationale === "string" ? item.rationale : "",
|
|
610
648
|
}));
|
|
611
649
|
}
|
|
612
650
|
recoveryCandidates.push(
|
|
@@ -622,7 +660,7 @@ export function buildContextEngineFactory(
|
|
|
622
660
|
);
|
|
623
661
|
}
|
|
624
662
|
|
|
625
|
-
const fittedRecovery =
|
|
663
|
+
const fittedRecovery = fitPromptBudgetFirstFit(
|
|
626
664
|
dedupeRecoveryCandidates(recoveryCandidates),
|
|
627
665
|
recoveryReserveTokens,
|
|
628
666
|
);
|
|
@@ -667,6 +705,11 @@ export function buildContextEngineFactory(
|
|
|
667
705
|
? {
|
|
668
706
|
recoveryTriggerFired: recoveryTrigger.fire,
|
|
669
707
|
crossSessionRawRecovery,
|
|
708
|
+
recoveryReserveTokens,
|
|
709
|
+
temporalQueryIndicator: temporalQuery.indicator,
|
|
710
|
+
temporalQueryActive: temporalQuery.active,
|
|
711
|
+
temporalQueryPatterns: temporalQuery.matchedPatterns,
|
|
712
|
+
temporalRecoverySlots: temporalRecoveryResult?.slots,
|
|
670
713
|
rawUserRecoveryCandidates: rawUserRecoveryDebug,
|
|
671
714
|
}
|
|
672
715
|
: undefined,
|
package/src/memory-provider.ts
CHANGED
|
@@ -1,87 +1,25 @@
|
|
|
1
|
+
import type { MemoryPromptSectionBuilder } from "openclaw/plugin-sdk/plugin-entry";
|
|
1
2
|
import type { PluginConfig, RecallCache, SearchResult } from "./types.js";
|
|
2
3
|
import type { RpcGetter } from "./plugin-runtime.js";
|
|
3
|
-
import { scoreCandidates } from "./scoring.js";
|
|
4
|
-
import { fitPromptBudget } from "./tokens.js";
|
|
5
|
-
import { buildMemoryHeader } from "./recall-utils.js";
|
|
6
4
|
|
|
7
|
-
const
|
|
5
|
+
const MEMORY_PROMPT_HEADER = [
|
|
6
|
+
"## Memory",
|
|
7
|
+
"LibraVDB persistent memory is configured. Recalled memories may appear",
|
|
8
|
+
"in context via the context-engine assembler when available and relevant.",
|
|
9
|
+
"",
|
|
10
|
+
] as const;
|
|
8
11
|
|
|
9
12
|
export function buildMemoryPromptSection(
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
):
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
citationsMode?: string;
|
|
22
|
-
messages?: Array<{ role: string; content: string }>;
|
|
23
|
-
userId?: string;
|
|
24
|
-
}): Promise<string[]> {
|
|
25
|
-
const queryText = params.messages?.at(-1)?.content ?? "";
|
|
26
|
-
const userId = params.userId ?? "default";
|
|
27
|
-
|
|
28
|
-
if (!queryText) {
|
|
29
|
-
return [
|
|
30
|
-
"## Memory",
|
|
31
|
-
"LibraVDB persistent memory is active. Recalled memories will appear",
|
|
32
|
-
"in context via the context-engine assembler when relevant.",
|
|
33
|
-
"",
|
|
34
|
-
];
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const rpc = await getRpc();
|
|
38
|
-
|
|
39
|
-
const [userHitsResult, globalHitsResult] = await Promise.all([
|
|
40
|
-
rpc.call<{ results: SearchResult[] }>("search_text", {
|
|
41
|
-
collection: `user:${userId}`,
|
|
42
|
-
text: queryText,
|
|
43
|
-
k: Math.ceil((cfg.topK ?? 8) / 2),
|
|
44
|
-
}),
|
|
45
|
-
rpc.call<{ results: SearchResult[] }>("search_text", {
|
|
46
|
-
collection: "global",
|
|
47
|
-
text: queryText,
|
|
48
|
-
k: Math.ceil((cfg.topK ?? 8) / 4),
|
|
49
|
-
}),
|
|
50
|
-
]);
|
|
51
|
-
|
|
52
|
-
const userHits = userHitsResult.results;
|
|
53
|
-
const globalHits = globalHitsResult.results;
|
|
54
|
-
|
|
55
|
-
recallCache.put({
|
|
56
|
-
userId,
|
|
57
|
-
queryText,
|
|
58
|
-
durableVariantHits: [],
|
|
59
|
-
userHits,
|
|
60
|
-
globalHits,
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
const ranked = scoreCandidates([...userHits, ...globalHits], {
|
|
64
|
-
alpha: cfg.alpha,
|
|
65
|
-
beta: cfg.beta,
|
|
66
|
-
gamma: cfg.gamma,
|
|
67
|
-
sessionId: "",
|
|
68
|
-
userId,
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
const selected = fitPromptBudget(ranked, MEMORY_PROMPT_BUDGET);
|
|
72
|
-
const recallHeader = buildMemoryHeader(selected);
|
|
73
|
-
|
|
74
|
-
const lines: string[] = [
|
|
75
|
-
"## Memory",
|
|
76
|
-
"LibraVDB persistent memory is active. Recalled memories will appear",
|
|
77
|
-
"in context via the context-engine assembler when relevant.",
|
|
78
|
-
];
|
|
79
|
-
|
|
80
|
-
if (recallHeader) {
|
|
81
|
-
lines.push(...recallHeader.split("\n"));
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
lines.push("");
|
|
85
|
-
return lines;
|
|
13
|
+
_getRpc: RpcGetter,
|
|
14
|
+
_cfg: PluginConfig,
|
|
15
|
+
_recallCache: RecallCache<SearchResult>,
|
|
16
|
+
): MemoryPromptSectionBuilder {
|
|
17
|
+
return function memoryPromptSection({
|
|
18
|
+
availableTools: _availableTools,
|
|
19
|
+
citationsMode: _citationsMode,
|
|
20
|
+
}): string[] {
|
|
21
|
+
// OpenClaw builds the memory prompt section synchronously for embedded runs.
|
|
22
|
+
// Actual retrieval and ranking happen in the context engine during assemble().
|
|
23
|
+
return [...MEMORY_PROMPT_HEADER];
|
|
86
24
|
};
|
|
87
|
-
}
|
|
25
|
+
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
declare module "openclaw/plugin-sdk/plugin-entry" {
|
|
2
|
+
export type MemoryPromptSectionBuilder = (params: {
|
|
3
|
+
availableTools: Set<string>;
|
|
4
|
+
citationsMode?: string;
|
|
5
|
+
}) => string[];
|
|
6
|
+
|
|
2
7
|
interface OpenClawCliCommand {
|
|
3
8
|
commands?: OpenClawCliCommand[];
|
|
4
9
|
command(name: string): OpenClawCliCommand;
|
|
@@ -18,7 +23,7 @@ declare module "openclaw/plugin-sdk/plugin-entry" {
|
|
|
18
23
|
warn?(message: string): void;
|
|
19
24
|
};
|
|
20
25
|
registerContextEngine(id: string, factory: () => unknown): void;
|
|
21
|
-
registerMemoryPromptSection(builder:
|
|
26
|
+
registerMemoryPromptSection(builder: MemoryPromptSectionBuilder): void;
|
|
22
27
|
registerMemoryFlushPlan?(resolver: unknown): void;
|
|
23
28
|
registerMemoryRuntime?(runtime: unknown): void;
|
|
24
29
|
registerMemoryEmbeddingProvider?(provider: unknown): void;
|
package/src/scoring.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SearchResult } from "./types.js";
|
|
2
|
+
import { getTemporalAnchorDensity } from "./temporal.js";
|
|
2
3
|
|
|
3
4
|
interface HybridOptions {
|
|
4
5
|
alpha?: number;
|
|
@@ -41,6 +42,7 @@ interface RawUserRecoveryOptions {
|
|
|
41
42
|
export interface RawUserRecoveryDebugCandidate {
|
|
42
43
|
id: string;
|
|
43
44
|
text: string;
|
|
45
|
+
temporalAnchorDensity: number;
|
|
44
46
|
semanticScore: number;
|
|
45
47
|
lexicalCoverage: number;
|
|
46
48
|
recencyScore: number;
|
|
@@ -319,17 +321,29 @@ export function rankRawUserRecoveryCandidates(
|
|
|
319
321
|
const now = opts.nowMs ?? Date.now();
|
|
320
322
|
const recencyLambda = Math.max(0, opts.recencyLambda ?? 0.00001);
|
|
321
323
|
const keywords = extractKeywords(opts.queryText);
|
|
324
|
+
const intentPhrases = extractIntentPhrases(opts.queryText);
|
|
322
325
|
|
|
323
326
|
const ranked = items
|
|
324
327
|
.map((item) => {
|
|
325
328
|
const semanticScore = clamp01(typeof item.score === "number" ? item.score : 0);
|
|
326
329
|
const lexicalCoverage = normalizedKeywordCoverage(keywords, item.text);
|
|
327
330
|
const recencyScore = computeRecencyScore(item, now, recencyLambda);
|
|
328
|
-
const
|
|
331
|
+
const temporalAnchorDensity = getTemporalAnchorDensity(
|
|
332
|
+
`${typeof item.metadata.collection === "string" ? item.metadata.collection : "unknown"}::${item.id}`,
|
|
333
|
+
item.text,
|
|
334
|
+
);
|
|
335
|
+
const intentAlignmentBonus = computeIntentAlignmentBonus(item.text, intentPhrases);
|
|
336
|
+
const finalScore = clamp01(
|
|
337
|
+
(0.30 * semanticScore) +
|
|
338
|
+
(0.60 * lexicalCoverage) +
|
|
339
|
+
(0.10 * recencyScore) +
|
|
340
|
+
intentAlignmentBonus,
|
|
341
|
+
);
|
|
329
342
|
const rationale = buildRawUserRecoveryRationale({
|
|
330
343
|
semanticScore,
|
|
331
344
|
lexicalCoverage,
|
|
332
345
|
recencyScore,
|
|
346
|
+
intentAlignmentBonus,
|
|
333
347
|
});
|
|
334
348
|
|
|
335
349
|
return {
|
|
@@ -340,6 +354,7 @@ export function rankRawUserRecoveryCandidates(
|
|
|
340
354
|
debug: {
|
|
341
355
|
id: item.id,
|
|
342
356
|
text: item.text,
|
|
357
|
+
temporalAnchorDensity,
|
|
343
358
|
semanticScore,
|
|
344
359
|
lexicalCoverage,
|
|
345
360
|
recencyScore,
|
|
@@ -473,7 +488,11 @@ function buildRawUserRecoveryRationale(scores: {
|
|
|
473
488
|
semanticScore: number;
|
|
474
489
|
lexicalCoverage: number;
|
|
475
490
|
recencyScore: number;
|
|
491
|
+
intentAlignmentBonus: number;
|
|
476
492
|
}): string {
|
|
493
|
+
if (scores.intentAlignmentBonus >= 0.04) {
|
|
494
|
+
return "intent phrase overlap lifted this candidate toward the query's direct ask";
|
|
495
|
+
}
|
|
477
496
|
const lexicalDelta = scores.lexicalCoverage - scores.semanticScore;
|
|
478
497
|
if (lexicalDelta > 0.15) {
|
|
479
498
|
return "lexical coverage lifted this candidate above its semantic score";
|
|
@@ -487,6 +506,79 @@ function buildRawUserRecoveryRationale(scores: {
|
|
|
487
506
|
return "semantic and lexical scores were balanced";
|
|
488
507
|
}
|
|
489
508
|
|
|
509
|
+
function computeIntentAlignmentBonus(text: string, intentPhrases: string[]): number {
|
|
510
|
+
if (intentPhrases.length === 0) {
|
|
511
|
+
return 0;
|
|
512
|
+
}
|
|
513
|
+
const normalized = normalizeTextForPhraseMatch(text);
|
|
514
|
+
const matched = intentPhrases.filter((phrase) => normalized.includes(phrase)).length;
|
|
515
|
+
if (matched === 0) {
|
|
516
|
+
return 0;
|
|
517
|
+
}
|
|
518
|
+
return Math.min(0.08, matched * 0.02);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function extractIntentPhrases(text: string): string[] {
|
|
522
|
+
const terms = normalizeTerms(text).filter((term) => !INTENT_STOPWORDS.has(term));
|
|
523
|
+
const phrases: string[] = [];
|
|
524
|
+
for (let size = 4; size >= 2; size -= 1) {
|
|
525
|
+
for (let i = 0; i <= terms.length - size; i += 1) {
|
|
526
|
+
const phraseTerms = terms.slice(i, i + size);
|
|
527
|
+
if (phraseTerms.some((term) => term.length < 3)) {
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
const phrase = phraseTerms.join(" ");
|
|
531
|
+
if (!phrases.includes(phrase)) {
|
|
532
|
+
phrases.push(phrase);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return phrases.slice(0, 12);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function normalizeTextForPhraseMatch(text: string): string {
|
|
540
|
+
return normalizeTerms(text).join(" ");
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const INTENT_STOPWORDS = new Set([
|
|
544
|
+
"the",
|
|
545
|
+
"and",
|
|
546
|
+
"for",
|
|
547
|
+
"with",
|
|
548
|
+
"that",
|
|
549
|
+
"this",
|
|
550
|
+
"have",
|
|
551
|
+
"from",
|
|
552
|
+
"your",
|
|
553
|
+
"what",
|
|
554
|
+
"when",
|
|
555
|
+
"where",
|
|
556
|
+
"which",
|
|
557
|
+
"would",
|
|
558
|
+
"could",
|
|
559
|
+
"should",
|
|
560
|
+
"about",
|
|
561
|
+
"into",
|
|
562
|
+
"some",
|
|
563
|
+
"before",
|
|
564
|
+
"after",
|
|
565
|
+
"them",
|
|
566
|
+
"they",
|
|
567
|
+
"been",
|
|
568
|
+
"just",
|
|
569
|
+
"want",
|
|
570
|
+
"looking",
|
|
571
|
+
"look",
|
|
572
|
+
"help",
|
|
573
|
+
"need",
|
|
574
|
+
"recommend",
|
|
575
|
+
"suggestions",
|
|
576
|
+
"suggest",
|
|
577
|
+
"advice",
|
|
578
|
+
"think",
|
|
579
|
+
"also",
|
|
580
|
+
]);
|
|
581
|
+
|
|
490
582
|
function extractKeywords(text: string): string[] {
|
|
491
583
|
const tokens = normalizeTerms(text);
|
|
492
584
|
const seen = new Set<string>();
|
package/src/sidecar.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
1
2
|
import net from "node:net";
|
|
2
3
|
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
@@ -268,13 +269,42 @@ export function daemonProvisioningHint(): string {
|
|
|
268
269
|
}
|
|
269
270
|
|
|
270
271
|
export function defaultEndpoint(platform = process.platform, homeDir = os.homedir()): string {
|
|
272
|
+
// Honour the daemon's own env var first (set by Homebrew LaunchAgent / systemd unit).
|
|
273
|
+
const envEndpoint = process.env.LIBRAVDB_RPC_ENDPOINT?.trim();
|
|
274
|
+
if (envEndpoint && isConfiguredEndpoint(envEndpoint)) {
|
|
275
|
+
return envEndpoint;
|
|
276
|
+
}
|
|
277
|
+
|
|
271
278
|
if (platform === "win32") {
|
|
272
279
|
return "tcp:127.0.0.1:37421";
|
|
273
280
|
}
|
|
281
|
+
|
|
282
|
+
const sockName = "libravdb.sock";
|
|
283
|
+
const candidateDirs = [
|
|
284
|
+
// User-local (npm plugin convention)
|
|
285
|
+
homeDir?.trim() ? path.join(homeDir, ".clawdb", "run") : null,
|
|
286
|
+
// Homebrew (Apple Silicon) — matches the Homebrew formula LaunchAgent
|
|
287
|
+
"/opt/homebrew/var/clawdb/run",
|
|
288
|
+
// Homebrew (Intel Mac) / manual Linux installs
|
|
289
|
+
"/usr/local/var/clawdb/run",
|
|
290
|
+
].filter((d): d is string => d !== null);
|
|
291
|
+
|
|
292
|
+
for (const dir of candidateDirs) {
|
|
293
|
+
const sockPath = path.join(dir, sockName);
|
|
294
|
+
try {
|
|
295
|
+
if (fs.existsSync(sockPath)) {
|
|
296
|
+
return `unix:${sockPath}`;
|
|
297
|
+
}
|
|
298
|
+
} catch {
|
|
299
|
+
// Permission error or similar — skip this candidate.
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Fallback to the original user-local path so error messages stay familiar.
|
|
274
304
|
const baseDir = homeDir?.trim()
|
|
275
305
|
? path.join(homeDir, ".clawdb", "run")
|
|
276
306
|
: path.join(".", ".clawdb", "run");
|
|
277
|
-
return `unix:${path.join(baseDir,
|
|
307
|
+
return `unix:${path.join(baseDir, sockName)}`;
|
|
278
308
|
}
|
|
279
309
|
|
|
280
310
|
export function buildSidecarEnv(cfg: PluginConfig): Record<string, string> {
|