@xdarkicex/openclaw-memory-libravdb 1.4.5 → 1.4.6
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/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/context-engine.ts +207 -13
- package/src/index.ts +1 -1
- package/src/plugin-runtime.ts +4 -1
- package/src/rpc.ts +14 -6
- package/src/sidecar.ts +114 -17
- package/src/temporal.ts +21 -0
- package/src/types.ts +1 -0
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/src/context-engine.ts
CHANGED
|
@@ -31,6 +31,7 @@ import type {
|
|
|
31
31
|
ContextCompactArgs,
|
|
32
32
|
ContextIngestArgs,
|
|
33
33
|
GatingResult,
|
|
34
|
+
LoggerLike,
|
|
34
35
|
MemoryMessage,
|
|
35
36
|
PluginConfig,
|
|
36
37
|
RecallCache,
|
|
@@ -50,10 +51,81 @@ const SESSION_STATE_COLLECTION_PREFIX = "session_state:";
|
|
|
50
51
|
const AFTER_TURN_DEDUPE_TTL_MS = 60 * 60 * 1000;
|
|
51
52
|
const AFTER_TURN_DEDUPE_MAX_ENTRIES = 1024;
|
|
52
53
|
|
|
54
|
+
function mapTemporalRecoveryDebugCandidates(
|
|
55
|
+
ranking: TemporalRecoveryRankingResult,
|
|
56
|
+
): NonNullable<NonNullable<ContextAssembleResult["_debug"]>["rawUserRecoveryCandidates"]> {
|
|
57
|
+
return ranking.debug.slice(0, 8).map((item) => ({
|
|
58
|
+
id: item.id,
|
|
59
|
+
text: item.text,
|
|
60
|
+
selected: "selected" in item ? Boolean(item.selected) : false,
|
|
61
|
+
tokenEstimate: estimateTokens(item.text),
|
|
62
|
+
temporalAnchorDensity: "temporalAnchorDensity" in item && typeof item.temporalAnchorDensity === "number"
|
|
63
|
+
? item.temporalAnchorDensity
|
|
64
|
+
: 0,
|
|
65
|
+
semanticScore: "semanticScore" in item && typeof item.semanticScore === "number"
|
|
66
|
+
? item.semanticScore
|
|
67
|
+
: 0,
|
|
68
|
+
slotCoverage: "slotCoverage" in item && typeof item.slotCoverage === "number"
|
|
69
|
+
? item.slotCoverage
|
|
70
|
+
: undefined,
|
|
71
|
+
slotMatches: "slotMatches" in item && Array.isArray(item.slotMatches)
|
|
72
|
+
? item.slotMatches
|
|
73
|
+
: undefined,
|
|
74
|
+
lexicalCoverage: "lexicalCoverage" in item && typeof item.lexicalCoverage === "number"
|
|
75
|
+
? item.lexicalCoverage
|
|
76
|
+
: ("slotCoverage" in item && typeof item.slotCoverage === "number" ? item.slotCoverage : 0),
|
|
77
|
+
recencyScore: "recencyScore" in item && typeof item.recencyScore === "number"
|
|
78
|
+
? item.recencyScore
|
|
79
|
+
: 0,
|
|
80
|
+
finalScore: typeof item.finalScore === "number" ? item.finalScore : 0,
|
|
81
|
+
rationale: typeof item.rationale === "string" ? item.rationale : "",
|
|
82
|
+
comparisonSide: "comparisonSide" in item && (item.comparisonSide === 0 || item.comparisonSide === 1 || item.comparisonSide === null)
|
|
83
|
+
? item.comparisonSide
|
|
84
|
+
: undefined,
|
|
85
|
+
comparisonSlot: "comparisonSlot" in item && typeof item.comparisonSlot === "string"
|
|
86
|
+
? item.comparisonSlot
|
|
87
|
+
: undefined,
|
|
88
|
+
comparisonSlotRecall: "comparisonSlotRecall" in item && typeof item.comparisonSlotRecall === "number"
|
|
89
|
+
? item.comparisonSlotRecall
|
|
90
|
+
: undefined,
|
|
91
|
+
comparisonSlotPrecision: "comparisonSlotPrecision" in item && typeof item.comparisonSlotPrecision === "number"
|
|
92
|
+
? item.comparisonSlotPrecision
|
|
93
|
+
: undefined,
|
|
94
|
+
comparisonSlotSpecificity: "comparisonSlotSpecificity" in item && typeof item.comparisonSlotSpecificity === "number"
|
|
95
|
+
? item.comparisonSlotSpecificity
|
|
96
|
+
: undefined,
|
|
97
|
+
comparisonSlotPositionWeightedRecall: "comparisonSlotPositionWeightedRecall" in item && typeof item.comparisonSlotPositionWeightedRecall === "number"
|
|
98
|
+
? item.comparisonSlotPositionWeightedRecall
|
|
99
|
+
: undefined,
|
|
100
|
+
comparisonSlotPositionWeightedPrecision: "comparisonSlotPositionWeightedPrecision" in item && typeof item.comparisonSlotPositionWeightedPrecision === "number"
|
|
101
|
+
? item.comparisonSlotPositionWeightedPrecision
|
|
102
|
+
: undefined,
|
|
103
|
+
comparisonSlotPositionWeightedSpecificity: "comparisonSlotPositionWeightedSpecificity" in item && typeof item.comparisonSlotPositionWeightedSpecificity === "number"
|
|
104
|
+
? item.comparisonSlotPositionWeightedSpecificity
|
|
105
|
+
: undefined,
|
|
106
|
+
comparisonFirstPersonClauseCount: "comparisonFirstPersonClauseCount" in item && typeof item.comparisonFirstPersonClauseCount === "number"
|
|
107
|
+
? item.comparisonFirstPersonClauseCount
|
|
108
|
+
: undefined,
|
|
109
|
+
comparisonProspectivePersonalVerbCount: "comparisonProspectivePersonalVerbCount" in item && typeof item.comparisonProspectivePersonalVerbCount === "number"
|
|
110
|
+
? item.comparisonProspectivePersonalVerbCount
|
|
111
|
+
: undefined,
|
|
112
|
+
comparisonPlanningDensity: "comparisonPlanningDensity" in item && typeof item.comparisonPlanningDensity === "number"
|
|
113
|
+
? item.comparisonPlanningDensity
|
|
114
|
+
: undefined,
|
|
115
|
+
comparisonPastness: "comparisonPastness" in item && typeof item.comparisonPastness === "number"
|
|
116
|
+
? item.comparisonPastness
|
|
117
|
+
: undefined,
|
|
118
|
+
comparisonSideWitnessScore: "comparisonSideWitnessScore" in item && typeof item.comparisonSideWitnessScore === "number"
|
|
119
|
+
? item.comparisonSideWitnessScore
|
|
120
|
+
: undefined,
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
|
|
53
124
|
export function buildContextEngineFactory(
|
|
54
125
|
getRpc: RpcGetter,
|
|
55
126
|
cfg: PluginConfig,
|
|
56
127
|
recallCache: RecallCache<SearchResult>,
|
|
128
|
+
logger: LoggerLike = console,
|
|
57
129
|
) {
|
|
58
130
|
let authoredHardCache: SearchResult[] | null = null;
|
|
59
131
|
let authoredSoftCache: SearchResult[] | null = null;
|
|
@@ -77,6 +149,9 @@ export function buildContextEngineFactory(
|
|
|
77
149
|
ownsCompaction: true,
|
|
78
150
|
async bootstrap({ sessionId, sessionKey, userId }: ContextBootstrapArgs) {
|
|
79
151
|
const durableNamespace = resolveDurableNamespace({ userId, sessionKey, fallback: `session:${sessionId}` });
|
|
152
|
+
logger.info?.(
|
|
153
|
+
`[libravdb] bootstrap sessionId=${sessionId} userId=${userId ?? "(none)"} sessionKey=${sessionKey ?? "(none)"} → durable=${durableNamespace}`,
|
|
154
|
+
);
|
|
80
155
|
const rpc = await getRpc();
|
|
81
156
|
await rpc.call("ensure_collections", {
|
|
82
157
|
collections: [
|
|
@@ -117,6 +192,7 @@ export function buildContextEngineFactory(
|
|
|
117
192
|
const result = await ingestCanonicalMessage({
|
|
118
193
|
getRpc,
|
|
119
194
|
cfg,
|
|
195
|
+
logger,
|
|
120
196
|
recallCache,
|
|
121
197
|
clearElevatedCacheForSession,
|
|
122
198
|
sessionId,
|
|
@@ -158,6 +234,7 @@ export function buildContextEngineFactory(
|
|
|
158
234
|
const result = await ingestCanonicalMessage({
|
|
159
235
|
getRpc,
|
|
160
236
|
cfg,
|
|
237
|
+
logger,
|
|
161
238
|
recallCache,
|
|
162
239
|
clearElevatedCacheForSession,
|
|
163
240
|
sessionId,
|
|
@@ -255,7 +332,7 @@ export function buildContextEngineFactory(
|
|
|
255
332
|
},
|
|
256
333
|
emit() {
|
|
257
334
|
for (const line of this.lines()) {
|
|
258
|
-
|
|
335
|
+
logger.info?.(line);
|
|
259
336
|
}
|
|
260
337
|
},
|
|
261
338
|
};
|
|
@@ -522,6 +599,14 @@ export function buildContextEngineFactory(
|
|
|
522
599
|
}),
|
|
523
600
|
]);
|
|
524
601
|
|
|
602
|
+
if (!dreamMode) {
|
|
603
|
+
logger.info?.(
|
|
604
|
+
`[libravdb] assemble recall durable=${durableNamespace} session=${sessionSearchCollection} sessionHits=${sessionHits.results.length} userHits=${userHits.results.length} globalHits=${globalHits.results.length}`,
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
let temporalRecoveryPreviewDebug: NonNullable<NonNullable<ContextAssembleResult["_debug"]>["rawUserRecoveryCandidates"]> = [];
|
|
609
|
+
|
|
525
610
|
if (!cached && !dreamMode) {
|
|
526
611
|
recallCache.put({
|
|
527
612
|
userId: durableNamespace,
|
|
@@ -573,6 +658,17 @@ export function buildContextEngineFactory(
|
|
|
573
658
|
elevatedRecallCache.set(elevatedKey, elevatedHits.results);
|
|
574
659
|
}
|
|
575
660
|
|
|
661
|
+
if (process.env.LONGMEMEVAL_DEBUG_RANKING === "1" && temporalSelectorGuard.shouldApply && userHits.results.length > 0) {
|
|
662
|
+
const previewRanking = rankTemporalRecoveryCandidates(annotateCollection(userHits.results, `user:${durableNamespace}`), {
|
|
663
|
+
queryText,
|
|
664
|
+
maxSelected: 3,
|
|
665
|
+
nowMs: Date.now(),
|
|
666
|
+
recencyLambda: cfg.recencyLambdaUser ?? 0.00001,
|
|
667
|
+
selectionTokenBudget: Math.max(1, Math.min(memoryBudget, retrievalBudget)),
|
|
668
|
+
});
|
|
669
|
+
temporalRecoveryPreviewDebug = mapTemporalRecoveryDebugCandidates(previewRanking);
|
|
670
|
+
}
|
|
671
|
+
|
|
576
672
|
profiler?.mark("rank");
|
|
577
673
|
const ranked = rankSection7VariantCandidates(
|
|
578
674
|
[
|
|
@@ -728,7 +824,10 @@ export function buildContextEngineFactory(
|
|
|
728
824
|
rawUserRecoveryDebug = reranked.debug.slice(0, 8).map((item) => ({
|
|
729
825
|
id: item.id,
|
|
730
826
|
text: item.text,
|
|
731
|
-
|
|
827
|
+
// This debug surface mirrors the recovery ranker itself. Packing and dedupe can
|
|
828
|
+
// legitimately drop a ranked candidate later, but the host-flow temporal test needs
|
|
829
|
+
// to see what the ranker selected before those downstream filters run.
|
|
830
|
+
selected: "selected" in item ? Boolean(item.selected) : false,
|
|
732
831
|
tokenEstimate: estimateTokens(item.text),
|
|
733
832
|
temporalAnchorDensity: "temporalAnchorDensity" in item && typeof item.temporalAnchorDensity === "number"
|
|
734
833
|
? item.temporalAnchorDensity
|
|
@@ -790,6 +889,90 @@ export function buildContextEngineFactory(
|
|
|
790
889
|
? item.comparisonSideWitnessScore
|
|
791
890
|
: undefined,
|
|
792
891
|
}));
|
|
892
|
+
|
|
893
|
+
// If the raw-turn recovery path gets fully deduped because the same turns were already
|
|
894
|
+
// surfaced through the normal durable user collection, expose the ranker output from
|
|
895
|
+
// that durable view as debug-only fallback. This keeps the host-flow temporal test
|
|
896
|
+
// aligned with the actual ranking behavior without changing prompt assembly.
|
|
897
|
+
if (rawUserRecoveryDebug.length === 0 && userHits.results.length > 0) {
|
|
898
|
+
const debugUserRanking = temporalRecoveryResult
|
|
899
|
+
? temporalRecoveryResult
|
|
900
|
+
: rankTemporalRecoveryCandidates(
|
|
901
|
+
annotateCollection(userHits.results, `user:${durableNamespace}`),
|
|
902
|
+
{
|
|
903
|
+
queryText,
|
|
904
|
+
maxSelected: 3,
|
|
905
|
+
nowMs: Date.now(),
|
|
906
|
+
recencyLambda: cfg.recencyLambdaUser ?? 0.00001,
|
|
907
|
+
selectionTokenBudget: recoveryReserveTokens,
|
|
908
|
+
},
|
|
909
|
+
);
|
|
910
|
+
rawUserRecoveryDebug = debugUserRanking.debug.slice(0, 8).map((item) => ({
|
|
911
|
+
id: item.id,
|
|
912
|
+
text: item.text,
|
|
913
|
+
selected: "selected" in item ? Boolean(item.selected) : false,
|
|
914
|
+
tokenEstimate: estimateTokens(item.text),
|
|
915
|
+
temporalAnchorDensity: "temporalAnchorDensity" in item && typeof item.temporalAnchorDensity === "number"
|
|
916
|
+
? item.temporalAnchorDensity
|
|
917
|
+
: 0,
|
|
918
|
+
semanticScore: "semanticScore" in item && typeof item.semanticScore === "number"
|
|
919
|
+
? item.semanticScore
|
|
920
|
+
: 0,
|
|
921
|
+
slotCoverage: "slotCoverage" in item && typeof item.slotCoverage === "number"
|
|
922
|
+
? item.slotCoverage
|
|
923
|
+
: undefined,
|
|
924
|
+
slotMatches: "slotMatches" in item && Array.isArray(item.slotMatches)
|
|
925
|
+
? item.slotMatches
|
|
926
|
+
: undefined,
|
|
927
|
+
lexicalCoverage: "lexicalCoverage" in item && typeof item.lexicalCoverage === "number"
|
|
928
|
+
? item.lexicalCoverage
|
|
929
|
+
: ("slotCoverage" in item && typeof item.slotCoverage === "number" ? item.slotCoverage : 0),
|
|
930
|
+
recencyScore: "recencyScore" in item && typeof item.recencyScore === "number"
|
|
931
|
+
? item.recencyScore
|
|
932
|
+
: 0,
|
|
933
|
+
finalScore: typeof item.finalScore === "number" ? item.finalScore : 0,
|
|
934
|
+
rationale: typeof item.rationale === "string" ? item.rationale : "",
|
|
935
|
+
comparisonSide: "comparisonSide" in item && (item.comparisonSide === 0 || item.comparisonSide === 1 || item.comparisonSide === null)
|
|
936
|
+
? item.comparisonSide
|
|
937
|
+
: undefined,
|
|
938
|
+
comparisonSlot: "comparisonSlot" in item && typeof item.comparisonSlot === "string"
|
|
939
|
+
? item.comparisonSlot
|
|
940
|
+
: undefined,
|
|
941
|
+
comparisonSlotRecall: "comparisonSlotRecall" in item && typeof item.comparisonSlotRecall === "number"
|
|
942
|
+
? item.comparisonSlotRecall
|
|
943
|
+
: undefined,
|
|
944
|
+
comparisonSlotPrecision: "comparisonSlotPrecision" in item && typeof item.comparisonSlotPrecision === "number"
|
|
945
|
+
? item.comparisonSlotPrecision
|
|
946
|
+
: undefined,
|
|
947
|
+
comparisonSlotSpecificity: "comparisonSlotSpecificity" in item && typeof item.comparisonSlotSpecificity === "number"
|
|
948
|
+
? item.comparisonSlotSpecificity
|
|
949
|
+
: undefined,
|
|
950
|
+
comparisonSlotPositionWeightedRecall: "comparisonSlotPositionWeightedRecall" in item && typeof item.comparisonSlotPositionWeightedRecall === "number"
|
|
951
|
+
? item.comparisonSlotPositionWeightedRecall
|
|
952
|
+
: undefined,
|
|
953
|
+
comparisonSlotPositionWeightedPrecision: "comparisonSlotPositionWeightedPrecision" in item && typeof item.comparisonSlotPositionWeightedPrecision === "number"
|
|
954
|
+
? item.comparisonSlotPositionWeightedPrecision
|
|
955
|
+
: undefined,
|
|
956
|
+
comparisonSlotPositionWeightedSpecificity: "comparisonSlotPositionWeightedSpecificity" in item && typeof item.comparisonSlotPositionWeightedSpecificity === "number"
|
|
957
|
+
? item.comparisonSlotPositionWeightedSpecificity
|
|
958
|
+
: undefined,
|
|
959
|
+
comparisonFirstPersonClauseCount: "comparisonFirstPersonClauseCount" in item && typeof item.comparisonFirstPersonClauseCount === "number"
|
|
960
|
+
? item.comparisonFirstPersonClauseCount
|
|
961
|
+
: undefined,
|
|
962
|
+
comparisonProspectivePersonalVerbCount: "comparisonProspectivePersonalVerbCount" in item && typeof item.comparisonProspectivePersonalVerbCount === "number"
|
|
963
|
+
? item.comparisonProspectivePersonalVerbCount
|
|
964
|
+
: undefined,
|
|
965
|
+
comparisonPlanningDensity: "comparisonPlanningDensity" in item && typeof item.comparisonPlanningDensity === "number"
|
|
966
|
+
? item.comparisonPlanningDensity
|
|
967
|
+
: undefined,
|
|
968
|
+
comparisonPastness: "comparisonPastness" in item && typeof item.comparisonPastness === "number"
|
|
969
|
+
? item.comparisonPastness
|
|
970
|
+
: undefined,
|
|
971
|
+
comparisonSideWitnessScore: "comparisonSideWitnessScore" in item && typeof item.comparisonSideWitnessScore === "number"
|
|
972
|
+
? item.comparisonSideWitnessScore
|
|
973
|
+
: undefined,
|
|
974
|
+
}));
|
|
975
|
+
}
|
|
793
976
|
}
|
|
794
977
|
recoveryCandidates.push(
|
|
795
978
|
...reranked.ranked.map((item) => {
|
|
@@ -836,18 +1019,16 @@ export function buildContextEngineFactory(
|
|
|
836
1019
|
}));
|
|
837
1020
|
}
|
|
838
1021
|
if (debugRecovery && rawUserRecoveryDebug.length > 0) {
|
|
839
|
-
const selectedIDs = new Set(
|
|
840
|
-
fittedRecovery
|
|
841
|
-
.filter((item) => item.metadata.recovery_scope === "user_turns")
|
|
842
|
-
.map((item: SearchResult) => item.id),
|
|
843
|
-
);
|
|
844
1022
|
rawUserRecoveryDebug = rawUserRecoveryDebug.map((item) => ({
|
|
845
1023
|
...item,
|
|
846
|
-
selected: selectedIDs.has(item.id),
|
|
847
1024
|
}));
|
|
848
1025
|
}
|
|
849
1026
|
}
|
|
850
1027
|
|
|
1028
|
+
if (debugRecovery && rawUserRecoveryDebug.length === 0 && temporalRecoveryPreviewDebug.length > 0) {
|
|
1029
|
+
rawUserRecoveryDebug = temporalRecoveryPreviewDebug;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
851
1032
|
const selected = [
|
|
852
1033
|
...hardItems,
|
|
853
1034
|
...tailBaseItems,
|
|
@@ -1189,6 +1370,7 @@ function clampFraction(value: number | undefined): number {
|
|
|
1189
1370
|
async function ingestCanonicalMessage(params: {
|
|
1190
1371
|
getRpc: RpcGetter;
|
|
1191
1372
|
cfg: PluginConfig;
|
|
1373
|
+
logger: LoggerLike;
|
|
1192
1374
|
recallCache: RecallCache<SearchResult>;
|
|
1193
1375
|
clearElevatedCacheForSession: (sessionId: string) => void;
|
|
1194
1376
|
sessionId: string;
|
|
@@ -1233,7 +1415,10 @@ async function ingestCanonicalMessage(params: {
|
|
|
1233
1415
|
if (useSessionRecallProjection(params.cfg) && !params.skipProjectionRebuild) {
|
|
1234
1416
|
await rebuildSessionRecallProjection(rpc, params.cfg, params.sessionId);
|
|
1235
1417
|
}
|
|
1236
|
-
} catch {
|
|
1418
|
+
} catch (error) {
|
|
1419
|
+
params.logger.error(
|
|
1420
|
+
`[libravdb] session ingest failed for ${params.sessionId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
1421
|
+
);
|
|
1237
1422
|
return { ingested: false };
|
|
1238
1423
|
}
|
|
1239
1424
|
|
|
@@ -1262,7 +1447,11 @@ async function ingestCanonicalMessage(params: {
|
|
|
1262
1447
|
// User turns must be stored durably regardless of gating score so the agent can recall
|
|
1263
1448
|
// friends, preferences, and context across sessions. Gating signals are still
|
|
1264
1449
|
// recorded in metadata for observability, but do not gate the insert.
|
|
1265
|
-
|
|
1450
|
+
//
|
|
1451
|
+
// IMPORTANT: This insert MUST be awaited. A previous fire-and-forget pattern
|
|
1452
|
+
// (void + catch) caused silent data loss when the daemon was slow/overloaded —
|
|
1453
|
+
// the user-level collection stayed empty, breaking cross-session recall entirely.
|
|
1454
|
+
await rpc.call("insert_text", {
|
|
1266
1455
|
collection: `user:${durableNamespace}`,
|
|
1267
1456
|
id: `${durableNamespace}:${turnId}`,
|
|
1268
1457
|
text: normalized.content,
|
|
@@ -1286,9 +1475,14 @@ async function ingestCanonicalMessage(params: {
|
|
|
1286
1475
|
gating_gconv: gating.gconv,
|
|
1287
1476
|
gating_gtech: gating.gtech,
|
|
1288
1477
|
},
|
|
1289
|
-
})
|
|
1290
|
-
} catch {
|
|
1291
|
-
// Session storage already happened;
|
|
1478
|
+
});
|
|
1479
|
+
} catch (userInsertError) {
|
|
1480
|
+
// Session storage already happened; log durable promotion failure visibly
|
|
1481
|
+
// so it does not silently vanish in production.
|
|
1482
|
+
params.logger.error(
|
|
1483
|
+
`[libravdb] durable user insert failed for ${durableNamespace}: ${userInsertError instanceof Error ? userInsertError.message : String(userInsertError)}`,
|
|
1484
|
+
);
|
|
1485
|
+
return { ingested: false };
|
|
1292
1486
|
}
|
|
1293
1487
|
|
|
1294
1488
|
return { ingested: true };
|
package/src/index.ts
CHANGED
|
@@ -32,7 +32,7 @@ export default definePluginEntry({
|
|
|
32
32
|
|
|
33
33
|
registerMemoryCli(api, runtime, cfg, api.logger ?? console);
|
|
34
34
|
api.registerContextEngine("libravdb-memory", () =>
|
|
35
|
-
buildContextEngineFactory(runtime.getRpc, cfg, recallCache),
|
|
35
|
+
buildContextEngineFactory(runtime.getRpc, cfg, recallCache, api.logger ?? console),
|
|
36
36
|
);
|
|
37
37
|
api.registerMemoryPromptSection(buildMemoryPromptSection(runtime.getRpc, cfg, recallCache));
|
|
38
38
|
api.registerMemoryRuntime?.(buildMemoryRuntimeBridge(runtime.getRpc, cfg));
|
package/src/plugin-runtime.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { LoggerLike, PluginConfig, SidecarHandle } from "./types.js";
|
|
|
4
4
|
|
|
5
5
|
export type RpcGetter = () => Promise<RpcClient>;
|
|
6
6
|
export const DEFAULT_RPC_TIMEOUT_MS = 30000;
|
|
7
|
+
export const STARTUP_HEALTH_TIMEOUT_MS = 2000;
|
|
7
8
|
|
|
8
9
|
export interface LifecycleHint {
|
|
9
10
|
hook: "before_reset" | "session_end";
|
|
@@ -43,7 +44,9 @@ export function createPluginRuntime(
|
|
|
43
44
|
const rpc = new RpcClient(sidecar.socket, {
|
|
44
45
|
timeoutMs: cfg.rpcTimeoutMs ?? DEFAULT_RPC_TIMEOUT_MS,
|
|
45
46
|
});
|
|
46
|
-
const health = await rpc.call<{ ok?: boolean; message?: string }>("health", {}
|
|
47
|
+
const health = await rpc.call<{ ok?: boolean; message?: string }>("health", {}, {
|
|
48
|
+
timeoutMs: STARTUP_HEALTH_TIMEOUT_MS,
|
|
49
|
+
});
|
|
47
50
|
if (!health.ok) {
|
|
48
51
|
try {
|
|
49
52
|
await sidecar.shutdown();
|
package/src/rpc.ts
CHANGED
|
@@ -18,20 +18,28 @@ export class RpcClient {
|
|
|
18
18
|
socket.setEncoding("utf8");
|
|
19
19
|
socket.on("data", (chunk) => this.handleData(chunk));
|
|
20
20
|
socket.on("close", () => this.rejectAll(new Error("Socket closed")));
|
|
21
|
+
socket.on("error", (error) => this.rejectAll(error));
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
async call<T>(method: string, _params: unknown): Promise<T> {
|
|
24
|
+
async call<T>(method: string, _params: unknown, callOptions: Partial<RpcCallOptions> = {}): Promise<T> {
|
|
24
25
|
return await new Promise<T>((resolve, reject) => {
|
|
25
26
|
const id = ++this.seq;
|
|
27
|
+
const timeoutMs = callOptions.timeoutMs ?? this.options.timeoutMs;
|
|
26
28
|
const timer = setTimeout(() => {
|
|
27
29
|
this.pending.delete(id);
|
|
28
|
-
reject(new Error(`RPC timeout: ${method} (${
|
|
29
|
-
},
|
|
30
|
+
reject(new Error(`RPC timeout: ${method} (${timeoutMs}ms)`));
|
|
31
|
+
}, timeoutMs);
|
|
30
32
|
|
|
31
33
|
this.pending.set(id, { resolve, reject, timer });
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
try {
|
|
35
|
+
this.socket.write(
|
|
36
|
+
`${JSON.stringify({ jsonrpc: "2.0", id, method, params: _params })}\n`,
|
|
37
|
+
);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
this.pending.delete(id);
|
|
41
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
42
|
+
}
|
|
35
43
|
});
|
|
36
44
|
}
|
|
37
45
|
|
package/src/sidecar.ts
CHANGED
|
@@ -9,6 +9,10 @@ type CloseHandler = () => void;
|
|
|
9
9
|
type DataHandler = (chunk: string) => void;
|
|
10
10
|
type ErrorHandler = (error: Error) => void;
|
|
11
11
|
|
|
12
|
+
const STARTUP_CONNECT_MAX_RETRIES = 5;
|
|
13
|
+
const STARTUP_CONNECT_BASE_DELAY_MS = 100;
|
|
14
|
+
const STARTUP_CONNECT_MAX_TOTAL_WAIT_MS = 2000;
|
|
15
|
+
|
|
12
16
|
export interface SidecarRuntime {
|
|
13
17
|
resolveEndpoint(cfg: PluginConfig): string | Promise<string>;
|
|
14
18
|
createSocket(endpoint: string): SidecarSocket;
|
|
@@ -18,6 +22,7 @@ export interface SidecarRuntime {
|
|
|
18
22
|
class PlaceholderSocket implements SidecarSocket {
|
|
19
23
|
private readonly onData = new Set<DataHandler>();
|
|
20
24
|
private readonly onClose = new Set<CloseHandler>();
|
|
25
|
+
private readonly onError = new Set<ErrorHandler>();
|
|
21
26
|
private readonly connectOnce = new Set<CloseHandler>();
|
|
22
27
|
private readonly errorOnce = new Set<ErrorHandler>();
|
|
23
28
|
|
|
@@ -32,11 +37,15 @@ class PlaceholderSocket implements SidecarSocket {
|
|
|
32
37
|
|
|
33
38
|
setEncoding(_encoding: string): void {}
|
|
34
39
|
|
|
35
|
-
on(event: "data" | "close", handler: DataHandler | CloseHandler): void {
|
|
40
|
+
on(event: "data" | "close" | "error", handler: DataHandler | CloseHandler | ErrorHandler): void {
|
|
36
41
|
if (event === "data") {
|
|
37
42
|
this.onData.add(handler as DataHandler);
|
|
38
43
|
return;
|
|
39
44
|
}
|
|
45
|
+
if (event === "error") {
|
|
46
|
+
this.onError.add(handler as ErrorHandler);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
40
49
|
this.onClose.add(handler as CloseHandler);
|
|
41
50
|
}
|
|
42
51
|
|
|
@@ -61,10 +70,7 @@ class PlaceholderSocket implements SidecarSocket {
|
|
|
61
70
|
}
|
|
62
71
|
} catch (error) {
|
|
63
72
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
64
|
-
|
|
65
|
-
handler(err);
|
|
66
|
-
}
|
|
67
|
-
this.errorOnce.clear();
|
|
73
|
+
this.emitError(err);
|
|
68
74
|
}
|
|
69
75
|
}
|
|
70
76
|
|
|
@@ -73,11 +79,22 @@ class PlaceholderSocket implements SidecarSocket {
|
|
|
73
79
|
handler();
|
|
74
80
|
}
|
|
75
81
|
}
|
|
82
|
+
|
|
83
|
+
private emitError(error: Error): void {
|
|
84
|
+
for (const handler of this.onError) {
|
|
85
|
+
handler(error);
|
|
86
|
+
}
|
|
87
|
+
for (const handler of this.errorOnce) {
|
|
88
|
+
handler(error);
|
|
89
|
+
}
|
|
90
|
+
this.errorOnce.clear();
|
|
91
|
+
}
|
|
76
92
|
}
|
|
77
93
|
|
|
78
94
|
class SupervisorSocket implements SidecarSocket {
|
|
79
95
|
private readonly onData = new Set<DataHandler>();
|
|
80
96
|
private readonly onClose = new Set<CloseHandler>();
|
|
97
|
+
private readonly onError = new Set<ErrorHandler>();
|
|
81
98
|
private readonly connectOnce = new Set<CloseHandler>();
|
|
82
99
|
private readonly errorOnce = new Set<ErrorHandler>();
|
|
83
100
|
private current?: SidecarSocket;
|
|
@@ -102,10 +119,24 @@ class SupervisorSocket implements SidecarSocket {
|
|
|
102
119
|
if (generation !== this.generation) {
|
|
103
120
|
return;
|
|
104
121
|
}
|
|
122
|
+
this.current = undefined;
|
|
105
123
|
for (const handler of this.onClose) {
|
|
106
124
|
handler();
|
|
107
125
|
}
|
|
108
126
|
});
|
|
127
|
+
socket.on("error", (error) => {
|
|
128
|
+
if (generation !== this.generation) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
this.current = undefined;
|
|
132
|
+
for (const handler of this.onError) {
|
|
133
|
+
handler(error);
|
|
134
|
+
}
|
|
135
|
+
for (const handler of this.errorOnce) {
|
|
136
|
+
handler(error);
|
|
137
|
+
}
|
|
138
|
+
this.errorOnce.clear();
|
|
139
|
+
});
|
|
109
140
|
|
|
110
141
|
for (const handler of this.connectOnce) {
|
|
111
142
|
handler();
|
|
@@ -118,11 +149,15 @@ class SupervisorSocket implements SidecarSocket {
|
|
|
118
149
|
this.current?.setEncoding(encoding);
|
|
119
150
|
}
|
|
120
151
|
|
|
121
|
-
on(event: "data" | "close", handler: DataHandler | CloseHandler): void {
|
|
152
|
+
on(event: "data" | "close" | "error", handler: DataHandler | CloseHandler | ErrorHandler): void {
|
|
122
153
|
if (event === "data") {
|
|
123
154
|
this.onData.add(handler as DataHandler);
|
|
124
155
|
return;
|
|
125
156
|
}
|
|
157
|
+
if (event === "error") {
|
|
158
|
+
this.onError.add(handler as ErrorHandler);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
126
161
|
this.onClose.add(handler as CloseHandler);
|
|
127
162
|
}
|
|
128
163
|
|
|
@@ -154,6 +189,7 @@ class SidecarSupervisor implements SidecarHandle {
|
|
|
154
189
|
private retries = 0;
|
|
155
190
|
private degraded = false;
|
|
156
191
|
private shuttingDown = false;
|
|
192
|
+
private reconnectScheduled = false;
|
|
157
193
|
public socket: SidecarSocket;
|
|
158
194
|
|
|
159
195
|
constructor(
|
|
@@ -166,7 +202,8 @@ class SidecarSupervisor implements SidecarHandle {
|
|
|
166
202
|
|
|
167
203
|
async start(): Promise<SidecarSocket> {
|
|
168
204
|
const endpoint = await this.runtime.resolveEndpoint(this.cfg);
|
|
169
|
-
const socket = await this.
|
|
205
|
+
const socket = await this.connectEndpointWithRetry(endpoint);
|
|
206
|
+
this.reconnectScheduled = false;
|
|
170
207
|
if (this.socket instanceof SupervisorSocket) {
|
|
171
208
|
this.socket.bind(socket);
|
|
172
209
|
} else {
|
|
@@ -184,21 +221,48 @@ class SidecarSupervisor implements SidecarHandle {
|
|
|
184
221
|
this.socket.destroy();
|
|
185
222
|
}
|
|
186
223
|
|
|
187
|
-
private async
|
|
188
|
-
const socket = this.runtime.createSocket(endpoint);
|
|
189
|
-
socket.on("close", () => {
|
|
190
|
-
void this.handleExit(1);
|
|
191
|
-
});
|
|
192
|
-
|
|
224
|
+
private async connectEndpointWithRetry(endpoint: string): Promise<SidecarSocket> {
|
|
193
225
|
if (isTcpEndpoint(endpoint)) {
|
|
194
226
|
this.logger.info?.(`[libravdb] using TCP endpoint ${endpoint}`);
|
|
195
227
|
} else {
|
|
196
228
|
this.logger.info?.(`[libravdb] using Unix socket ${endpoint}`);
|
|
197
229
|
}
|
|
198
230
|
|
|
231
|
+
let waitedMs = 0;
|
|
232
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
233
|
+
try {
|
|
234
|
+
return await this.connectEndpoint(endpoint);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
if (!isStartupConnectRetryableError(error) || attempt >= STARTUP_CONNECT_MAX_RETRIES - 1) {
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const delayMs = computeStartupConnectRetryDelay(attempt, waitedMs);
|
|
241
|
+
if (delayMs <= 0) {
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
waitedMs += delayMs;
|
|
245
|
+
this.logger.info?.(
|
|
246
|
+
`[libravdb] Daemon not ready, retrying connection (attempt ${attempt + 1}/${STARTUP_CONNECT_MAX_RETRIES})...`,
|
|
247
|
+
);
|
|
248
|
+
await sleep(delayMs);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private async connectEndpoint(endpoint: string): Promise<SidecarSocket> {
|
|
254
|
+
const socket = this.runtime.createSocket(endpoint);
|
|
199
255
|
return await new Promise<SidecarSocket>((resolve, reject) => {
|
|
200
|
-
socket.once("connect", () =>
|
|
201
|
-
|
|
256
|
+
socket.once("connect", () => {
|
|
257
|
+
socket.on("close", () => {
|
|
258
|
+
void this.handleExit(1);
|
|
259
|
+
});
|
|
260
|
+
resolve(socket);
|
|
261
|
+
});
|
|
262
|
+
socket.once("error", (error) => {
|
|
263
|
+
socket.destroy();
|
|
264
|
+
reject(formatConnectionError(endpoint, error));
|
|
265
|
+
});
|
|
202
266
|
});
|
|
203
267
|
}
|
|
204
268
|
|
|
@@ -209,6 +273,9 @@ class SidecarSupervisor implements SidecarHandle {
|
|
|
209
273
|
if (code === 0) {
|
|
210
274
|
return;
|
|
211
275
|
}
|
|
276
|
+
if (this.reconnectScheduled) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
212
279
|
|
|
213
280
|
const maxRetries = this.cfg.maxRetries ?? 3;
|
|
214
281
|
if (this.retries >= maxRetries) {
|
|
@@ -219,8 +286,10 @@ class SidecarSupervisor implements SidecarHandle {
|
|
|
219
286
|
|
|
220
287
|
const backoffMs = computeBackoffMs(this.retries);
|
|
221
288
|
this.retries += 1;
|
|
289
|
+
this.reconnectScheduled = true;
|
|
222
290
|
this.runtime.scheduleRestart(backoffMs, () => {
|
|
223
291
|
void this.start().catch((error) => {
|
|
292
|
+
this.reconnectScheduled = false;
|
|
224
293
|
const message = error instanceof Error ? error.message : String(error);
|
|
225
294
|
this.logger.error(`[libravdb] sidecar reconnect failed: ${message}`);
|
|
226
295
|
});
|
|
@@ -242,6 +311,17 @@ export function computeBackoffMs(retries: number): number {
|
|
|
242
311
|
return Math.min(500 * Math.pow(2, retries), 16000);
|
|
243
312
|
}
|
|
244
313
|
|
|
314
|
+
export function computeStartupConnectRetryDelay(attempt: number, waitedMs = 0): number {
|
|
315
|
+
if (attempt < 0) {
|
|
316
|
+
return 0;
|
|
317
|
+
}
|
|
318
|
+
const remainingMs = STARTUP_CONNECT_MAX_TOTAL_WAIT_MS - waitedMs;
|
|
319
|
+
if (remainingMs <= 0) {
|
|
320
|
+
return 0;
|
|
321
|
+
}
|
|
322
|
+
return Math.min(STARTUP_CONNECT_BASE_DELAY_MS * Math.pow(2, attempt), remainingMs);
|
|
323
|
+
}
|
|
324
|
+
|
|
245
325
|
export function isTcpEndpoint(endpoint: string): boolean {
|
|
246
326
|
return endpoint.startsWith("tcp:");
|
|
247
327
|
}
|
|
@@ -423,16 +503,29 @@ function createDefaultRuntime(): SidecarRuntime {
|
|
|
423
503
|
};
|
|
424
504
|
}
|
|
425
505
|
|
|
506
|
+
function isStartupConnectRetryableError(error: unknown): boolean {
|
|
507
|
+
const code = typeof (error as NodeJS.ErrnoException | undefined)?.code === "string"
|
|
508
|
+
? (error as NodeJS.ErrnoException).code
|
|
509
|
+
: "";
|
|
510
|
+
return code === "ENOENT" || code === "ECONNREFUSED";
|
|
511
|
+
}
|
|
512
|
+
|
|
426
513
|
function formatConnectionError(endpoint: string, error: Error): Error {
|
|
427
514
|
const code = typeof (error as NodeJS.ErrnoException).code === "string"
|
|
428
515
|
? (error as NodeJS.ErrnoException).code
|
|
429
516
|
: "";
|
|
517
|
+
const annotated = error instanceof Error ? error : new Error(String(error));
|
|
518
|
+
if (code) {
|
|
519
|
+
(annotated as NodeJS.ErrnoException).code = code;
|
|
520
|
+
}
|
|
430
521
|
if (code === "ENOENT" || code === "ECONNREFUSED") {
|
|
431
|
-
|
|
522
|
+
const unavailable = new Error(
|
|
432
523
|
`LibraVDB daemon unavailable at ${describeEndpoint(endpoint)}. ${daemonProvisioningHint()} Or set sidecarPath to a running daemon endpoint.`,
|
|
433
524
|
);
|
|
525
|
+
(unavailable as NodeJS.ErrnoException).code = code;
|
|
526
|
+
return unavailable;
|
|
434
527
|
}
|
|
435
|
-
return
|
|
528
|
+
return annotated;
|
|
436
529
|
}
|
|
437
530
|
|
|
438
531
|
function describeEndpoint(endpoint: string): string {
|
|
@@ -448,6 +541,10 @@ function isConfiguredEndpoint(value?: string): boolean {
|
|
|
448
541
|
|
|
449
542
|
export { PlaceholderSocket };
|
|
450
543
|
|
|
544
|
+
function sleep(delayMs: number): Promise<void> {
|
|
545
|
+
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
546
|
+
}
|
|
547
|
+
|
|
451
548
|
export async function probeSidecarEndpoint(cfg: PluginConfig): Promise<string | null> {
|
|
452
549
|
const endpoint = resolveConfiguredEndpoint(cfg);
|
|
453
550
|
try {
|
package/src/temporal.ts
CHANGED
|
@@ -279,6 +279,13 @@ export function rankTemporalRecoveryCandidates(
|
|
|
279
279
|
const temporalQuery = detectTemporalQuerySignal(opts.queryText);
|
|
280
280
|
const slots = extractTemporalSlots(opts.queryText);
|
|
281
281
|
const isComparisonQuery = temporalQuery.matchedPatterns.includes("first or earlier");
|
|
282
|
+
// Duration-interval queries ("how many days ... after", "how long ... since") require explicit
|
|
283
|
+
// date anchors to be answerable. The soft-blend scorer can otherwise let an anchor-free turn
|
|
284
|
+
// outrank anchor-bearing ones when the semantic signal is still high.
|
|
285
|
+
const isDurationIntervalQuery = !isComparisonQuery && (
|
|
286
|
+
temporalQuery.matchedPatterns.includes("how many days") ||
|
|
287
|
+
temporalQuery.matchedPatterns.includes("how long")
|
|
288
|
+
);
|
|
282
289
|
const effectiveSlots = isComparisonQuery ? filterComparisonSlots(slots) : slots;
|
|
283
290
|
const comparisonSlots = isComparisonQuery ? deriveComparisonSideSlots(effectiveSlots) : [];
|
|
284
291
|
const recencyLambda = Math.max(0, opts.recencyLambda ?? 0.00001);
|
|
@@ -299,6 +306,16 @@ export function rankTemporalRecoveryCandidates(
|
|
|
299
306
|
comparisonProfile.rawCandidateCount = items.length;
|
|
300
307
|
}
|
|
301
308
|
|
|
309
|
+
// Pre-scan anchor density only for duration-interval queries so the gate below can verify at
|
|
310
|
+
// least one anchor-bearing candidate exists before zeroing anchor-free ones. Cached, so the
|
|
311
|
+
// re-check inside the map is cheap.
|
|
312
|
+
const anyAnchorBearing = isDurationIntervalQuery && items.some((item) =>
|
|
313
|
+
getTemporalAnchorDensity(
|
|
314
|
+
`${typeof item.metadata.collection === "string" ? item.metadata.collection : "unknown"}::${item.id}`,
|
|
315
|
+
item.text,
|
|
316
|
+
) > 0,
|
|
317
|
+
);
|
|
318
|
+
|
|
302
319
|
const decorateStart = comparisonProfile ? process.hrtime.bigint() : 0n;
|
|
303
320
|
const decorated = items.map((item) => {
|
|
304
321
|
const semanticScore = clamp01(typeof item.finalScore === "number" ? item.finalScore : item.score ?? 0);
|
|
@@ -331,6 +348,10 @@ export function rankTemporalRecoveryCandidates(
|
|
|
331
348
|
comparisonSideWitnessScore,
|
|
332
349
|
temporalQueryActive: temporalQuery.active,
|
|
333
350
|
})
|
|
351
|
+
// Duration-interval gate: if we have at least one anchor-bearing candidate, anchor-free
|
|
352
|
+
// candidates cannot answer the date question and should stay below the greedy threshold.
|
|
353
|
+
: isDurationIntervalQuery && anyAnchorBearing && temporalAnchorDensity === 0
|
|
354
|
+
? 0
|
|
334
355
|
: (0.40 * semanticScore) +
|
|
335
356
|
(0.25 * recencyScore) +
|
|
336
357
|
(0.20 * temporalAnchorDensity) +
|
package/src/types.ts
CHANGED
|
@@ -152,6 +152,7 @@ export interface SidecarSocket {
|
|
|
152
152
|
setEncoding(encoding: string): void;
|
|
153
153
|
on(event: "data", handler: (chunk: string) => void): void;
|
|
154
154
|
on(event: "close", handler: () => void): void;
|
|
155
|
+
on(event: "error", handler: (error: Error) => void): void;
|
|
155
156
|
once(event: "connect", handler: () => void): void;
|
|
156
157
|
once(event: "error", handler: (error: Error) => void): void;
|
|
157
158
|
write(chunk: string): void;
|