@xdarkicex/openclaw-memory-libravdb 1.4.6 → 1.4.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.
Files changed (75) hide show
  1. package/HOOK.md +14 -0
  2. package/README.md +32 -2
  3. package/dist/cli.d.ts +39 -0
  4. package/dist/cli.js +208 -0
  5. package/dist/context-engine.d.ts +56 -0
  6. package/dist/context-engine.js +125 -0
  7. package/dist/dream-promotion.d.ts +47 -0
  8. package/dist/dream-promotion.js +363 -0
  9. package/dist/dream-routing.d.ts +6 -0
  10. package/dist/dream-routing.js +31 -0
  11. package/dist/durable-namespace.d.ts +6 -0
  12. package/dist/durable-namespace.js +24 -0
  13. package/dist/grpc-client.d.ts +23 -0
  14. package/dist/grpc-client.js +104 -0
  15. package/dist/index.d.ts +10 -0
  16. package/dist/index.js +40 -0
  17. package/dist/lifecycle-hooks.d.ts +4 -0
  18. package/dist/lifecycle-hooks.js +64 -0
  19. package/dist/markdown-hash.d.ts +3 -0
  20. package/dist/markdown-hash.js +82 -0
  21. package/dist/markdown-ingest.d.ts +43 -0
  22. package/dist/markdown-ingest.js +464 -0
  23. package/dist/memory-provider.d.ts +4 -0
  24. package/dist/memory-provider.js +13 -0
  25. package/dist/memory-runtime.d.ts +118 -0
  26. package/dist/memory-runtime.js +217 -0
  27. package/dist/plugin-runtime.d.ts +28 -0
  28. package/dist/plugin-runtime.js +127 -0
  29. package/dist/proto/intelligence_kernel/v1/kernel.proto +378 -0
  30. package/dist/recall-cache.d.ts +2 -0
  31. package/dist/recall-cache.js +30 -0
  32. package/dist/rpc-protobuf-codecs.d.ts +70 -0
  33. package/dist/rpc-protobuf-codecs.js +77 -0
  34. package/dist/rpc.d.ts +14 -0
  35. package/dist/rpc.js +121 -0
  36. package/dist/sidecar.d.ts +34 -0
  37. package/dist/sidecar.js +535 -0
  38. package/dist/types.d.ts +163 -0
  39. package/dist/types.js +1 -0
  40. package/docs/contributing.md +14 -13
  41. package/docs/install.md +7 -9
  42. package/docs/installation.md +23 -16
  43. package/docs/uninstall.md +1 -1
  44. package/index.js +2 -0
  45. package/openclaw.plugin.json +2 -2
  46. package/package.json +39 -16
  47. package/packaging/README.md +0 -71
  48. package/packaging/homebrew/libravdbd.rb.tmpl +0 -224
  49. package/packaging/launchd/com.xdarkicex.libravdbd.plist +0 -32
  50. package/packaging/systemd/libravdbd.service +0 -12
  51. package/src/cli.ts +0 -299
  52. package/src/comparison-experiments.ts +0 -128
  53. package/src/context-engine.ts +0 -1645
  54. package/src/continuity.ts +0 -93
  55. package/src/dream-promotion.ts +0 -492
  56. package/src/dream-routing.ts +0 -40
  57. package/src/durable-namespace.ts +0 -34
  58. package/src/index.ts +0 -47
  59. package/src/lifecycle-hooks.ts +0 -96
  60. package/src/markdown-hash.ts +0 -104
  61. package/src/markdown-ingest.ts +0 -627
  62. package/src/memory-provider.ts +0 -25
  63. package/src/memory-runtime.ts +0 -283
  64. package/src/openclaw-plugin-sdk.d.ts +0 -59
  65. package/src/plugin-runtime.ts +0 -119
  66. package/src/recall-cache.ts +0 -34
  67. package/src/recall-utils.ts +0 -131
  68. package/src/rpc.ts +0 -92
  69. package/src/scoring.ts +0 -632
  70. package/src/sidecar.ts +0 -583
  71. package/src/temporal.ts +0 -1031
  72. package/src/tokens.ts +0 -52
  73. package/src/types.ts +0 -278
  74. package/tsconfig.json +0 -20
  75. package/tsconfig.tests.json +0 -12
package/src/temporal.ts DELETED
@@ -1,1031 +0,0 @@
1
- import { estimateTokens } from "./tokens.js";
2
- import type { SearchResult } from "./types.js";
3
- import {
4
- createComparisonProfileSummary,
5
- resolveComparisonExperimentConfig,
6
- type ComparisonExperimentConfig,
7
- type ComparisonProfileSummary,
8
- } from "./comparison-experiments.js";
9
-
10
- const TEMPORAL_PATTERN_WEIGHTS: Array<{ label: string; weight: number; patterns: RegExp[] }> = [
11
- {
12
- label: "how many days",
13
- weight: 1.0,
14
- patterns: [/\bhow\s+many\s+days\b/i],
15
- },
16
- {
17
- label: "how long",
18
- weight: 0.9,
19
- patterns: [/\bhow\s+long\b/i],
20
- },
21
- {
22
- label: "before or after",
23
- weight: 0.8,
24
- patterns: [/\bbefore\b/i, /\bafter\b/i],
25
- },
26
- {
27
- label: "since or between",
28
- weight: 0.7,
29
- patterns: [/\bsince\b/i, /\bbetween\b/i],
30
- },
31
- {
32
- label: "first or earlier",
33
- weight: 0.8,
34
- patterns: [/\bfirst\b/i, /\bearlier\b/i, /\bwhich\s+came\s+first\b/i],
35
- },
36
- {
37
- label: "when did",
38
- weight: 0.7,
39
- patterns: [/\bwhen\s+did\b/i],
40
- },
41
- ];
42
-
43
- const TEMPORAL_ANCHOR_PATTERNS: RegExp[] = [
44
- /\b\d{4}-\d{2}-\d{2}\b/g,
45
- /\b\d{1,2}\/\d{1,2}(?:\/\d{2,4})?\b/g,
46
- /\b(?:jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|jul(?:y)?|aug(?:ust)?|sep(?:t(?:ember)?)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)\s+\d{1,2}(?:st|nd|rd|th)?(?:,\s*\d{4})?\b/gi,
47
- /\b(?:monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b/gi,
48
- /\b(?:today|yesterday|tomorrow|last\s+(?:week|month|year|night|saturday|sunday)|next\s+(?:week|month|year|monday|tuesday|wednesday|thursday|friday|saturday|sunday)|mid-?[a-z]+)\b/gi,
49
- /\b\d{1,2}:\d{2}(?:\s?[ap]m)?\b/gi,
50
- /\b(?:19|20)\d{2}\b/g,
51
- /\b\d{10,13}\b/g,
52
- ];
53
-
54
- const TEMPORAL_XI_NORM = 1.5;
55
- const TEMPORAL_XI_THRESHOLD = 0.3;
56
- const TEMPORAL_ANCHOR_NORM = 3;
57
- const TEMPORAL_ANCHOR_CACHE_MAX = 4096;
58
- const temporalAnchorCache = new Map<string, number>();
59
-
60
- const TEMPORAL_SLOT_STOPWORDS = new Set([
61
- "the",
62
- "and",
63
- "for",
64
- "with",
65
- "that",
66
- "this",
67
- "have",
68
- "from",
69
- "your",
70
- "what",
71
- "when",
72
- "where",
73
- "which",
74
- "would",
75
- "could",
76
- "should",
77
- "about",
78
- "into",
79
- "some",
80
- "them",
81
- "they",
82
- "been",
83
- "just",
84
- "want",
85
- "looking",
86
- "look",
87
- "help",
88
- "need",
89
- "recommend",
90
- "suggestions",
91
- "suggest",
92
- "advice",
93
- "think",
94
- "also",
95
- "did",
96
- "does",
97
- "do",
98
- "after",
99
- "before",
100
- "since",
101
- "between",
102
- "first",
103
- "earlier",
104
- "many",
105
- "days",
106
- "long",
107
- "how",
108
- "did",
109
- "take",
110
- "took",
111
- "it",
112
- "me",
113
- "my",
114
- "i",
115
- ]);
116
-
117
- const COMPARISON_GENERIC_SLOT_PREFIXES = new Set([
118
- "trip",
119
- "event",
120
- "device",
121
- "thing",
122
- "item",
123
- "time",
124
- "place",
125
- "activity",
126
- "experience",
127
- "job",
128
- "role",
129
- "project",
130
- "task",
131
- "purchase",
132
- "plan",
133
- "change",
134
- "decision",
135
- "move",
136
- "visit",
137
- "meeting",
138
- "session",
139
- ]);
140
-
141
- const COMPARISON_GENERIC_AFFILIATION_TERMS = new Set([
142
- ...COMPARISON_GENERIC_SLOT_PREFIXES,
143
- "solo",
144
- ]);
145
-
146
- const COMPARISON_FIRST_PERSON_CLAUSE_PATTERNS: RegExp[] = [
147
- /\bi\b/gi,
148
- /\bi'm\b/gi,
149
- /\bi've\b/gi,
150
- ];
151
-
152
- const COMPARISON_PROSPECTIVE_PERSONAL_PATTERNS: RegExp[] = [
153
- /\b(?:i\s+am\s+|i'm\s+)?considering\b/gi,
154
- /\b(?:i\s+am\s+|i'm\s+)?planning\b/gi,
155
- /\bi\s+am\s+thinking\s+about\b/gi,
156
- /\bi'?m\s+thinking\s+about\b/gi,
157
- /\bi\s+have\s+been\s+looking\b/gi,
158
- /\bi'?ve\s+been\s+looking\b/gi,
159
- /\bi\s+am\s+looking\s+(?:at|into)\b/gi,
160
- /\bi'?m\s+looking\s+(?:at|into)\b/gi,
161
- /\bi\s+want\s+to\b/gi,
162
- /\bi\s+would\s+like\s+to\b/gi,
163
- /\bi\s+am\s+hoping\s+to\b/gi,
164
- /\bi'?m\s+hoping\s+to\b/gi,
165
- /\bi\s+am\s+going\s+to\s+(?:visit|go|try|do)\b/gi,
166
- /\bi'?m\s+going\s+to\s+(?:visit|go|try|do)\b/gi,
167
- /\bi\s+am\s+trying\s+to\s+decide\b/gi,
168
- /\bi'?m\s+trying\s+to\s+decide\b/gi,
169
- /\bi\s+am\s+trying\s+to\s+(?:plan|figure)\b/gi,
170
- /\bi'?m\s+trying\s+to\s+(?:plan|figure)\b/gi,
171
- ];
172
-
173
- export interface TemporalQuerySignal {
174
- indicator: number;
175
- active: boolean;
176
- matchedPatterns: string[];
177
- }
178
-
179
- export interface TemporalSelectorGuardDecision {
180
- shouldApply: boolean;
181
- slots: string[];
182
- reason: string;
183
- }
184
-
185
- export interface TemporalRecoveryDebugCandidate {
186
- id: string;
187
- text: string;
188
- selected: boolean;
189
- temporalAnchorDensity: number;
190
- semanticScore: number;
191
- recencyScore: number;
192
- slotCoverage: number;
193
- slotMatches: string[];
194
- finalScore: number;
195
- rationale: string;
196
- comparisonSide?: 0 | 1 | null;
197
- comparisonSlot?: string;
198
- comparisonSlotRecall?: number;
199
- comparisonSlotPrecision?: number;
200
- comparisonSlotSpecificity?: number;
201
- comparisonSlotPositionWeightedRecall?: number;
202
- comparisonSlotPositionWeightedPrecision?: number;
203
- comparisonSlotPositionWeightedSpecificity?: number;
204
- comparisonFirstPersonClauseCount?: number;
205
- comparisonProspectivePersonalVerbCount?: number;
206
- comparisonPlanningDensity?: number;
207
- comparisonPastness?: number;
208
- comparisonSideWitnessScore?: number;
209
- }
210
-
211
- export interface TemporalRecoveryRankingResult {
212
- ranked: SearchResult[];
213
- debug: TemporalRecoveryDebugCandidate[];
214
- temporalQuery: TemporalQuerySignal;
215
- slots: string[];
216
- comparisonCoverageApplied?: boolean;
217
- comparisonCoverageSlots?: string[];
218
- comparisonCoverageMinTokens?: number;
219
- comparisonWitnessIds?: string[];
220
- comparisonProfile?: ComparisonProfileSummary;
221
- }
222
-
223
- let activeComparisonProfile: ComparisonProfileSummary | null = null;
224
- let activeComparisonExperimentConfig: ComparisonExperimentConfig | null = null;
225
-
226
- export function detectTemporalQuerySignal(queryText: string): TemporalQuerySignal {
227
- const matchedPatterns: string[] = [];
228
- let weightedMatches = 0;
229
-
230
- for (const entry of TEMPORAL_PATTERN_WEIGHTS) {
231
- if (entry.patterns.some((pattern) => pattern.test(queryText))) {
232
- matchedPatterns.push(entry.label);
233
- weightedMatches += entry.weight;
234
- }
235
- }
236
-
237
- const indicator = clamp01(weightedMatches / TEMPORAL_XI_NORM);
238
- return {
239
- indicator,
240
- active: indicator >= TEMPORAL_XI_THRESHOLD,
241
- matchedPatterns,
242
- };
243
- }
244
-
245
- export function getTemporalAnchorDensity(docKey: string, text: string): number {
246
- const cacheKey = `${docKey}\n${text}`;
247
- const cached = temporalAnchorCache.get(cacheKey);
248
- if (typeof cached === "number") {
249
- touchTemporalAnchorCache(cacheKey, cached);
250
- return cached;
251
- }
252
-
253
- const uniqueMatches = new Set<string>();
254
- for (const pattern of TEMPORAL_ANCHOR_PATTERNS) {
255
- for (const match of text.matchAll(pattern)) {
256
- const value = match[0]?.trim().toLowerCase();
257
- if (value) {
258
- uniqueMatches.add(value);
259
- }
260
- }
261
- }
262
-
263
- const density = clamp01(uniqueMatches.size / TEMPORAL_ANCHOR_NORM);
264
- touchTemporalAnchorCache(cacheKey, density);
265
- return density;
266
- }
267
-
268
- export function rankTemporalRecoveryCandidates(
269
- items: SearchResult[],
270
- opts: {
271
- queryText: string;
272
- maxSelected?: number;
273
- nowMs?: number;
274
- recencyLambda?: number;
275
- selectionTokenBudget?: number;
276
- },
277
- ): TemporalRecoveryRankingResult {
278
- const comparisonExperiment = resolveComparisonExperimentConfig();
279
- const temporalQuery = detectTemporalQuerySignal(opts.queryText);
280
- const slots = extractTemporalSlots(opts.queryText);
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
- );
289
- const effectiveSlots = isComparisonQuery ? filterComparisonSlots(slots) : slots;
290
- const comparisonSlots = isComparisonQuery ? deriveComparisonSideSlots(effectiveSlots) : [];
291
- const recencyLambda = Math.max(0, opts.recencyLambda ?? 0.00001);
292
- const now = opts.nowMs ?? Date.now();
293
- const maxSelected = Math.max(1, Math.floor(opts.maxSelected ?? 3));
294
- const selectionTokenBudget = Math.max(0, Math.floor(opts.selectionTokenBudget ?? Number.MAX_SAFE_INTEGER));
295
- const comparisonProfile = comparisonExperiment.profilingEnabled && isComparisonQuery
296
- ? createComparisonProfileSummary(comparisonExperiment.ablationMode)
297
- : null;
298
- const previousProfile = activeComparisonProfile;
299
- const previousExperimentConfig = activeComparisonExperimentConfig;
300
- activeComparisonProfile = comparisonProfile;
301
- activeComparisonExperimentConfig = comparisonExperiment;
302
- const totalStart = comparisonProfile ? process.hrtime.bigint() : 0n;
303
-
304
- try {
305
- if (comparisonProfile) {
306
- comparisonProfile.rawCandidateCount = items.length;
307
- }
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
-
319
- const decorateStart = comparisonProfile ? process.hrtime.bigint() : 0n;
320
- const decorated = items.map((item) => {
321
- const semanticScore = clamp01(typeof item.finalScore === "number" ? item.finalScore : item.score ?? 0);
322
- const recencyScore = computeRecencyScore(item, now, recencyLambda);
323
- const temporalAnchorDensity = getTemporalAnchorDensity(
324
- `${typeof item.metadata.collection === "string" ? item.metadata.collection : "unknown"}::${item.id}`,
325
- item.text,
326
- );
327
- const { coverage, matches } = computeSlotCoverage(effectiveSlots, item.text);
328
- const tokenEstimate = estimateTokensWithProfile(item.text);
329
- const comparisonSide = comparisonSlots.length === 2
330
- ? computeComparisonSideAffiliation(comparisonSlots, item.text)
331
- : null;
332
- const comparisonMetrics = comparisonSide !== null
333
- ? computeComparisonSlotSpecificityMetrics(
334
- comparisonSlots[comparisonSide]!,
335
- item.text,
336
- comparisonSlots[1 - comparisonSide],
337
- )
338
- : null;
339
- const comparisonSideWitnessScore = comparisonMetrics
340
- ? comparisonMetrics.sideWitnessScore
341
- : undefined;
342
- const finalScore = clamp01(isComparisonQuery
343
- ? computeComparisonFinalScore({
344
- semanticScore,
345
- recencyScore,
346
- temporalAnchorDensity,
347
- coverage,
348
- comparisonSideWitnessScore,
349
- temporalQueryActive: temporalQuery.active,
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
355
- : (0.40 * semanticScore) +
356
- (0.25 * recencyScore) +
357
- (0.20 * temporalAnchorDensity) +
358
- (0.15 * coverage) +
359
- (temporalQuery.active ? 0.05 : 0));
360
- return {
361
- item,
362
- semanticScore,
363
- recencyScore,
364
- temporalAnchorDensity,
365
- slotCoverage: coverage,
366
- slotMatches: matches,
367
- tokenEstimate,
368
- comparisonSide,
369
- comparisonMetrics,
370
- comparisonSideWitnessScore,
371
- finalScore,
372
- };
373
- });
374
- if (comparisonProfile) {
375
- comparisonProfile.decorateMs += elapsedMs(decorateStart);
376
- comparisonProfile.comparisonCandidateCount = decorated.filter((candidate) => candidate.comparisonSide !== null).length;
377
- comparisonProfile.side0AffiliatedCount = decorated.filter((candidate) => candidate.comparisonSide === 0).length;
378
- comparisonProfile.side1AffiliatedCount = decorated.filter((candidate) => candidate.comparisonSide === 1).length;
379
- }
380
-
381
- const selectedIDs = new Set<string>();
382
- const coveredSlots = new Set<string>();
383
- const selected: SearchResult[] = [];
384
- let comparisonCoverageApplied = false;
385
- let comparisonCoverageMinTokens: number | undefined;
386
- let comparisonWitnessIds: string[] | undefined;
387
-
388
- if (comparisonSlots.length === 2) {
389
- const sideCandidates = comparisonSlots.map((_, sideIndex) => {
390
- const candidates = decorated
391
- .filter((candidate) => candidate.comparisonSide === sideIndex);
392
- recordSort(candidates.length);
393
- return candidates.sort((left, right) => {
394
- const leftWitnessScore = left.comparisonSideWitnessScore ?? Number.NEGATIVE_INFINITY;
395
- const rightWitnessScore = right.comparisonSideWitnessScore ?? Number.NEGATIVE_INFINITY;
396
- if (rightWitnessScore !== leftWitnessScore) {
397
- return rightWitnessScore - leftWitnessScore;
398
- }
399
- if (right.finalScore !== left.finalScore) {
400
- return right.finalScore - left.finalScore;
401
- }
402
- return left.tokenEstimate - right.tokenEstimate;
403
- });
404
- });
405
- const cheapestSideTokenSum = sideCandidates.reduce((sum, candidates) => (
406
- candidates.length > 0 ? sum + candidates.reduce((best, candidate) => Math.min(best, candidate.tokenEstimate), Number.POSITIVE_INFINITY) : sum
407
- ), 0);
408
- if (sideCandidates.every((candidates) => candidates.length > 0) && Number.isFinite(cheapestSideTokenSum)) {
409
- comparisonCoverageMinTokens = cheapestSideTokenSum;
410
- const bestPair = findBestComparisonCoveragePair(sideCandidates[0], sideCandidates[1], selectionTokenBudget);
411
- if (bestPair) {
412
- for (const candidate of bestPair) {
413
- selectedIDs.add(candidate.item.id);
414
- for (const slot of candidate.slotMatches) {
415
- coveredSlots.add(slot);
416
- }
417
- selected.push({
418
- ...candidate.item,
419
- finalScore: candidate.finalScore,
420
- });
421
- }
422
- comparisonCoverageApplied = true;
423
- comparisonWitnessIds = [bestPair[0].item.id, bestPair[1].item.id];
424
- }
425
- }
426
- }
427
-
428
- const greedyStart = comparisonProfile ? process.hrtime.bigint() : 0n;
429
- for (let pass = selected.length; pass < maxSelected; pass += 1) {
430
- let best: (typeof decorated)[number] | null = null;
431
- let bestScore = Number.NEGATIVE_INFINITY;
432
-
433
- for (const candidate of decorated) {
434
- if (selectedIDs.has(candidate.item.id)) {
435
- continue;
436
- }
437
- const marginalCoverage =
438
- candidate.slotMatches.filter((slot) => !coveredSlots.has(slot)).length / Math.max(1, effectiveSlots.length);
439
- const combined = candidate.finalScore + (0.25 * marginalCoverage);
440
- if (combined > bestScore) {
441
- best = candidate;
442
- bestScore = combined;
443
- }
444
- }
445
-
446
- if (!best || bestScore < 0.12) {
447
- break;
448
- }
449
-
450
- selectedIDs.add(best.item.id);
451
- for (const slot of best.slotMatches) {
452
- coveredSlots.add(slot);
453
- }
454
- selected.push({
455
- ...best.item,
456
- finalScore: best.finalScore,
457
- });
458
- }
459
- if (comparisonProfile) {
460
- comparisonProfile.greedyFillMs += elapsedMs(greedyStart);
461
- }
462
-
463
- const remainingCandidates = decorated
464
- .filter((candidate) => !selectedIDs.has(candidate.item.id));
465
- recordSort(remainingCandidates.length);
466
- const remaining = remainingCandidates
467
- .sort((left, right) => right.finalScore - left.finalScore)
468
- .map((candidate) => ({
469
- ...candidate.item,
470
- finalScore: candidate.finalScore,
471
- }));
472
-
473
- const ranked = [...selected, ...remaining];
474
- const debugStart = comparisonProfile ? process.hrtime.bigint() : 0n;
475
- const debugCandidates = [...decorated];
476
- recordSort(debugCandidates.length);
477
- const debug = debugCandidates
478
- .sort((left, right) => right.finalScore - left.finalScore)
479
- .map((candidate) => ({
480
- id: candidate.item.id,
481
- text: candidate.item.text,
482
- selected: selectedIDs.has(candidate.item.id),
483
- temporalAnchorDensity: candidate.temporalAnchorDensity,
484
- semanticScore: candidate.semanticScore,
485
- recencyScore: candidate.recencyScore,
486
- slotCoverage: candidate.slotCoverage,
487
- slotMatches: candidate.slotMatches,
488
- finalScore: candidate.finalScore,
489
- rationale: buildTemporalRecoveryRationale(candidate.slotCoverage, candidate.temporalAnchorDensity, candidate.semanticScore),
490
- comparisonSide: candidate.comparisonSide,
491
- comparisonSlot: candidate.comparisonSide !== null ? comparisonSlots[candidate.comparisonSide] : undefined,
492
- comparisonSlotRecall: candidate.comparisonMetrics?.recall,
493
- comparisonSlotPrecision: candidate.comparisonMetrics?.precision,
494
- comparisonSlotSpecificity: candidate.comparisonMetrics?.specificity,
495
- comparisonSlotPositionWeightedRecall: candidate.comparisonMetrics?.positionWeightedRecall,
496
- comparisonSlotPositionWeightedPrecision: candidate.comparisonMetrics?.positionWeightedPrecision,
497
- comparisonSlotPositionWeightedSpecificity: candidate.comparisonMetrics?.positionWeightedSpecificity,
498
- comparisonFirstPersonClauseCount: candidate.comparisonMetrics?.firstPersonClauseCount,
499
- comparisonProspectivePersonalVerbCount: candidate.comparisonMetrics?.prospectivePersonalVerbCount,
500
- comparisonPlanningDensity: candidate.comparisonMetrics?.planningDensity,
501
- comparisonPastness: candidate.comparisonMetrics?.pastness,
502
- comparisonSideWitnessScore: candidate.comparisonSideWitnessScore,
503
- }));
504
- if (comparisonProfile) {
505
- comparisonProfile.debugBuildMs += elapsedMs(debugStart);
506
- comparisonProfile.rankTotalMs += elapsedMs(totalStart);
507
- }
508
-
509
- return {
510
- ranked,
511
- debug,
512
- temporalQuery,
513
- slots: effectiveSlots,
514
- comparisonCoverageApplied,
515
- comparisonCoverageSlots: comparisonSlots.length === 2 ? comparisonSlots : undefined,
516
- comparisonCoverageMinTokens,
517
- comparisonWitnessIds,
518
- comparisonProfile: comparisonProfile ?? undefined,
519
- };
520
- } finally {
521
- activeComparisonProfile = previousProfile;
522
- activeComparisonExperimentConfig = previousExperimentConfig;
523
- }
524
- }
525
-
526
- export function decideTemporalSelectorGuard(
527
- queryText: string,
528
- temporalQuery: TemporalQuerySignal = detectTemporalQuerySignal(queryText),
529
- ): TemporalSelectorGuardDecision {
530
- const slots = extractTemporalSlots(queryText);
531
- if (!temporalQuery.active) {
532
- return {
533
- shouldApply: false,
534
- slots,
535
- reason: "temporal query gate inactive",
536
- };
537
- }
538
-
539
- const strongCompositionalPattern = temporalQuery.matchedPatterns.some((pattern) =>
540
- pattern === "how many days" ||
541
- pattern === "how long" ||
542
- pattern === "before or after" ||
543
- pattern === "since or between"
544
- );
545
- const strongComparisonPattern = temporalQuery.matchedPatterns.some((pattern) =>
546
- pattern === "first or earlier"
547
- );
548
- if (!strongCompositionalPattern && !strongComparisonPattern) {
549
- return {
550
- shouldApply: false,
551
- slots,
552
- reason: "query lacks strong compositional temporal pattern",
553
- };
554
- }
555
-
556
- if (strongComparisonPattern && !strongCompositionalPattern) {
557
- if (slots.length < 1 || slots.length > 4) {
558
- return {
559
- shouldApply: false,
560
- slots,
561
- reason: "comparison query did not resolve to a sensible number of slots",
562
- };
563
- }
564
- return {
565
- shouldApply: true,
566
- slots,
567
- reason: "comparison query with temporal entity extraction",
568
- };
569
- }
570
-
571
- if (slots.length !== 2) {
572
- return {
573
- shouldApply: false,
574
- slots,
575
- reason: "query did not resolve to exactly two temporal slots",
576
- };
577
- }
578
-
579
- return {
580
- shouldApply: true,
581
- slots,
582
- reason: "strong temporal query with two-slot decomposition",
583
- };
584
- }
585
-
586
- export function resetTemporalCachesForTest(): void {
587
- temporalAnchorCache.clear();
588
- }
589
-
590
- function extractTemporalSlots(text: string): string[] {
591
- const clauses = splitTemporalClauses(text);
592
- const slots = new Set<string>();
593
-
594
- for (const clause of clauses) {
595
- const terms = normalizeTerms(clause)
596
- .filter((term) => term.length >= 3 && !TEMPORAL_SLOT_STOPWORDS.has(term));
597
- if (terms.length === 0) {
598
- continue;
599
- }
600
- if (terms.length <= 3) {
601
- slots.add(terms.join(" "));
602
- continue;
603
- }
604
- slots.add(terms.slice(0, 4).join(" "));
605
- slots.add(terms.slice(-4).join(" "));
606
- }
607
-
608
- if (slots.size === 0) {
609
- const fallback = normalizeTerms(text).filter((term) => term.length >= 3 && !TEMPORAL_SLOT_STOPWORDS.has(term));
610
- if (fallback.length > 0) {
611
- slots.add(fallback.slice(0, 4).join(" "));
612
- }
613
- }
614
-
615
- return [...slots].slice(0, 4);
616
- }
617
-
618
- function computeSlotCoverage(slots: string[], candidateText: string): { coverage: number; matches: string[] } {
619
- if (slots.length === 0) {
620
- return { coverage: 0, matches: [] };
621
- }
622
-
623
- const candidateTerms = new Set(normalizeTerms(candidateText));
624
- const matches: string[] = [];
625
- let covered = 0;
626
-
627
- for (const slot of slots) {
628
- const slotTerms = normalizeTerms(slot).filter((term) => term.length >= 3);
629
- if (slotTerms.length === 0) {
630
- continue;
631
- }
632
- const overlap = slotTerms.filter((term) => candidateTerms.has(term)).length / slotTerms.length;
633
- if (overlap >= 0.5) {
634
- covered += 1;
635
- matches.push(slot);
636
- }
637
- }
638
-
639
- return {
640
- coverage: covered / Math.max(1, slots.length),
641
- matches,
642
- };
643
- }
644
-
645
- function filterComparisonSlots(slots: string[]): string[] {
646
- const filtered = slots.filter((slot) => {
647
- const terms = normalizeTerms(slot).filter(
648
- (term) => term.length >= 3 && !TEMPORAL_SLOT_STOPWORDS.has(term),
649
- );
650
- if (terms.length === 0) {
651
- return false;
652
- }
653
- if (terms.length === 1 && COMPARISON_GENERIC_SLOT_PREFIXES.has(terms[0]!)) {
654
- return false;
655
- }
656
- return true;
657
- });
658
-
659
- return filtered.length >= 1 ? filtered : slots;
660
- }
661
-
662
- function deriveComparisonSideSlots(slots: string[]): string[] {
663
- const specificSlots = slots.filter((slot) => {
664
- const terms = normalizeTerms(slot).filter(
665
- (term) => term.length >= 3 && !TEMPORAL_SLOT_STOPWORDS.has(term),
666
- );
667
- if (terms.length === 0) {
668
- return false;
669
- }
670
- return !(terms.length <= 2 && COMPARISON_GENERIC_SLOT_PREFIXES.has(terms[0]!));
671
- });
672
- const usableSlots = specificSlots.length >= 2 ? specificSlots : slots;
673
- return usableSlots.slice(0, 2);
674
- }
675
-
676
- function computeComparisonSideScore(slots: string[], candidateText: string): number {
677
- if (slots.length < 2) {
678
- return 0;
679
- }
680
-
681
- const overlaps = slots
682
- .map((slot) => computeSlotCoverage([slot], candidateText).coverage)
683
- .sort((left, right) => right - left);
684
- const strongest = overlaps[0] ?? 0;
685
- const second = overlaps[1] ?? 0;
686
- const asymmetry = Math.abs(strongest - second);
687
-
688
- return clamp01((0.6 * asymmetry) + (0.4 * strongest));
689
- }
690
-
691
- function computeComparisonSideAffiliation(slots: string[], candidateText: string): 0 | 1 | null {
692
- const start = activeComparisonProfile ? process.hrtime.bigint() : 0n;
693
- try {
694
- if (slots.length < 2) {
695
- return null;
696
- }
697
-
698
- const leftCoverage = activeComparisonExperimentConfig?.disableDiscriminativeAffiliation
699
- ? computeSlotCoverage([slots[0]!], candidateText).coverage
700
- : computeComparisonAffiliationCoverage(slots[0]!, slots[1]!, candidateText);
701
- const rightCoverage = activeComparisonExperimentConfig?.disableDiscriminativeAffiliation
702
- ? computeSlotCoverage([slots[1]!], candidateText).coverage
703
- : computeComparisonAffiliationCoverage(slots[1]!, slots[0]!, candidateText);
704
- if (leftCoverage >= 0.5 && leftCoverage > rightCoverage) {
705
- return 0;
706
- }
707
- if (rightCoverage >= 0.5 && rightCoverage > leftCoverage) {
708
- return 1;
709
- }
710
- return null;
711
- } finally {
712
- if (activeComparisonProfile) {
713
- activeComparisonProfile.sideAffiliationMs += elapsedMs(start);
714
- }
715
- }
716
- }
717
-
718
- function computeComparisonAffiliationCoverage(slot: string, otherSlot: string, candidateText: string): number {
719
- const slotTerms = normalizeTerms(slot).filter(
720
- (term) => term.length >= 3 && !TEMPORAL_SLOT_STOPWORDS.has(term),
721
- );
722
- const otherTerms = new Set(normalizeTerms(otherSlot).filter(
723
- (term) => term.length >= 3 && !TEMPORAL_SLOT_STOPWORDS.has(term),
724
- ));
725
- const discriminativeTerms = slotTerms.filter((term) => (
726
- !COMPARISON_GENERIC_AFFILIATION_TERMS.has(term) && !otherTerms.has(term)
727
- ));
728
- if (discriminativeTerms.length === 0) {
729
- return 0;
730
- }
731
-
732
- const candidateTerms = new Set(normalizeTerms(candidateText));
733
- const matched = discriminativeTerms.filter((term) => candidateTerms.has(term)).length;
734
- return matched / discriminativeTerms.length;
735
- }
736
-
737
- function computeComparisonSlotSpecificityMetrics(
738
- slot: string,
739
- candidateText: string,
740
- otherSlot?: string,
741
- ): {
742
- recall: number;
743
- precision: number;
744
- specificity: number;
745
- positionWeightedRecall: number;
746
- positionWeightedPrecision: number;
747
- positionWeightedSpecificity: number;
748
- firstPersonClauseCount: number;
749
- prospectivePersonalVerbCount: number;
750
- planningDensity: number;
751
- pastness: number;
752
- sideWitnessScore: number;
753
- } {
754
- const start = activeComparisonProfile ? process.hrtime.bigint() : 0n;
755
- try {
756
- const slotTerms = normalizeContentTerms(slot);
757
- const candidateTerms = normalizeContentTerms(candidateText);
758
- const candidateTermSequence = normalizeContentTermSequence(candidateText);
759
- const firstPositions = buildFirstContentTermPositions(candidateTermSequence);
760
- const matchedTerms = [...slotTerms].filter((term) => candidateTerms.has(term));
761
- const matched = matchedTerms.length;
762
- const positionWeightedMatched = matchedTerms.reduce((sum, term) => {
763
- const position = firstPositions.get(term);
764
- if (typeof position !== "number") {
765
- return sum;
766
- }
767
- const earlyThreshold = candidateTermSequence.length * 0.3;
768
- return sum + (position < earlyThreshold ? 1 : 0.5);
769
- }, 0);
770
- const recall = slotTerms.size > 0 ? matched / slotTerms.size : 0;
771
- const precision = matched / Math.max(5, candidateTerms.size);
772
- const useSpecificity = slotTerms.size >= 2;
773
- const specificity = recall * precision;
774
- const positionWeightedRecall = slotTerms.size > 0 ? positionWeightedMatched / slotTerms.size : 0;
775
- const positionWeightedPrecision = positionWeightedMatched / Math.max(5, candidateTerms.size);
776
- const positionWeightedSpecificity = positionWeightedRecall * positionWeightedPrecision;
777
- const { firstPersonClauseCount, prospectivePersonalVerbCount, planningDensity, pastness } = computePastness(candidateText);
778
- const otherSlotTerms = otherSlot ? normalizeContentTerms(otherSlot) : new Set<string>();
779
- const otherMatched = otherSlotTerms.size > 0
780
- ? [...otherSlotTerms].filter((term) => candidateTerms.has(term)).length
781
- : 0;
782
- const otherSlotRecall = otherSlotTerms.size > 0 ? otherMatched / otherSlotTerms.size : 0;
783
- const purity = clamp01(1 - otherSlotRecall);
784
- const purityMultiplier = activeComparisonExperimentConfig?.disableContaminationPenalty
785
- ? 1
786
- : 0.7 + (0.3 * purity);
787
- const rawWitnessScore = useSpecificity
788
- ? (activeComparisonExperimentConfig?.disableWitnessPositionPastness
789
- ? specificity
790
- : positionWeightedSpecificity * Math.max(0.6, pastness))
791
- : recall;
792
- return {
793
- recall,
794
- precision,
795
- specificity,
796
- positionWeightedRecall,
797
- positionWeightedPrecision,
798
- positionWeightedSpecificity,
799
- firstPersonClauseCount,
800
- prospectivePersonalVerbCount,
801
- planningDensity,
802
- pastness,
803
- sideWitnessScore: clamp01(rawWitnessScore * purityMultiplier),
804
- };
805
- } finally {
806
- if (activeComparisonProfile) {
807
- activeComparisonProfile.specificityMs += elapsedMs(start);
808
- }
809
- }
810
- }
811
-
812
- function findBestComparisonCoveragePair(
813
- leftCandidates: Array<{
814
- item: SearchResult;
815
- finalScore: number;
816
- comparisonSideWitnessScore?: number;
817
- tokenEstimate: number;
818
- slotMatches: string[];
819
- }>,
820
- rightCandidates: Array<{
821
- item: SearchResult;
822
- finalScore: number;
823
- comparisonSideWitnessScore?: number;
824
- tokenEstimate: number;
825
- slotMatches: string[];
826
- }>,
827
- selectionTokenBudget: number,
828
- ): Array<{
829
- item: SearchResult;
830
- finalScore: number;
831
- comparisonSideWitnessScore?: number;
832
- tokenEstimate: number;
833
- slotMatches: string[];
834
- }> | null {
835
- const start = activeComparisonProfile ? process.hrtime.bigint() : 0n;
836
- let bestPair: Array<{
837
- item: SearchResult;
838
- finalScore: number;
839
- comparisonSideWitnessScore?: number;
840
- tokenEstimate: number;
841
- slotMatches: string[];
842
- }> | null = null;
843
- let bestPairScore = Number.NEGATIVE_INFINITY;
844
-
845
- for (const left of leftCandidates) {
846
- for (const right of rightCandidates) {
847
- if (left.item.id === right.item.id) {
848
- continue;
849
- }
850
- const totalTokens = left.tokenEstimate + right.tokenEstimate;
851
- if (totalTokens > selectionTokenBudget) {
852
- continue;
853
- }
854
- const leftWitness = activeComparisonExperimentConfig?.disablePairScoreOnWitness
855
- ? left.finalScore
856
- : (left.comparisonSideWitnessScore ?? left.finalScore);
857
- const rightWitness = activeComparisonExperimentConfig?.disablePairScoreOnWitness
858
- ? right.finalScore
859
- : (right.comparisonSideWitnessScore ?? right.finalScore);
860
- const pairScore = leftWitness + rightWitness;
861
- const currentTokens = bestPair ? bestPair[0]!.tokenEstimate + bestPair[1]!.tokenEstimate : Number.POSITIVE_INFINITY;
862
- if (pairScore > bestPairScore || (pairScore === bestPairScore && totalTokens < currentTokens)) {
863
- bestPair = [left, right];
864
- bestPairScore = pairScore;
865
- }
866
- }
867
- }
868
- if (activeComparisonProfile) {
869
- activeComparisonProfile.pairSelectionMs += elapsedMs(start);
870
- }
871
- return bestPair;
872
- }
873
-
874
- function buildTemporalRecoveryRationale(slotCoverage: number, anchorDensity: number, semanticScore: number): string {
875
- if (slotCoverage >= 0.5 && anchorDensity >= 0.5) {
876
- return "slot coverage and temporal anchors both supported this candidate";
877
- }
878
- if (slotCoverage >= 0.5) {
879
- return "slot coverage lifted this candidate toward the query's subevents";
880
- }
881
- if (anchorDensity >= 0.5) {
882
- return "temporal anchors lifted this candidate toward the query's date logic";
883
- }
884
- if (semanticScore >= 0.6) {
885
- return "semantic similarity kept this candidate in the temporal pool";
886
- }
887
- return "candidate remained in the bounded temporal recovery pool";
888
- }
889
-
890
- function computeRecencyScore(item: SearchResult, now: number, recencyLambda: number): number {
891
- const ts = typeof item.metadata.ts === "number" ? item.metadata.ts : now;
892
- const ageSeconds = Math.max(0, now - ts) / 1000;
893
- return Math.exp(-recencyLambda * ageSeconds);
894
- }
895
-
896
- function splitTemporalClauses(text: string): string[] {
897
- return text
898
- .split(/(?:\bafter\b|\bbefore\b|\bbetween\b|\bor\b|\band\b|\bthen\b|[?.!,;\n]+)/i)
899
- .map((part) => part.trim())
900
- .filter((part) => part.length > 0);
901
- }
902
-
903
- function normalizeTerms(text: string): string[] {
904
- if (activeComparisonProfile) {
905
- activeComparisonProfile.normalizeTermsCalls += 1;
906
- }
907
- return text
908
- .toLowerCase()
909
- .split(/[^a-z0-9_]+/i)
910
- .filter((term) => term.length > 0);
911
- }
912
-
913
- function normalizeContentTerms(text: string): Set<string> {
914
- if (activeComparisonProfile) {
915
- activeComparisonProfile.normalizeContentTermsCalls += 1;
916
- }
917
- return new Set(
918
- normalizeContentTermSequence(text),
919
- );
920
- }
921
-
922
- function normalizeContentTermSequence(text: string): string[] {
923
- if (activeComparisonProfile) {
924
- activeComparisonProfile.normalizeContentTermsCalls += 1;
925
- }
926
- return normalizeTerms(text).filter((term) => term.length >= 3 && !TEMPORAL_SLOT_STOPWORDS.has(term));
927
- }
928
-
929
- function buildFirstContentTermPositions(terms: string[]): Map<string, number> {
930
- const positions = new Map<string, number>();
931
- for (let index = 0; index < terms.length; index += 1) {
932
- const term = terms[index]!;
933
- if (!positions.has(term)) {
934
- positions.set(term, index);
935
- }
936
- }
937
- return positions;
938
- }
939
-
940
- function computeComparisonFinalScore({
941
- semanticScore,
942
- recencyScore,
943
- temporalAnchorDensity,
944
- coverage,
945
- comparisonSideWitnessScore,
946
- temporalQueryActive,
947
- }: {
948
- semanticScore: number;
949
- recencyScore: number;
950
- temporalAnchorDensity: number;
951
- coverage: number;
952
- comparisonSideWitnessScore?: number;
953
- temporalQueryActive: boolean;
954
- }): number {
955
- if (activeComparisonExperimentConfig?.disableComparisonBlend) {
956
- return (0.40 * semanticScore) +
957
- (0.25 * recencyScore) +
958
- (0.20 * temporalAnchorDensity) +
959
- (0.15 * coverage) +
960
- (temporalQueryActive ? 0.05 : 0);
961
- }
962
-
963
- return (0.15 * semanticScore) +
964
- (0.15 * recencyScore) +
965
- (0.15 * coverage) +
966
- (0.55 * (comparisonSideWitnessScore ?? 0));
967
- }
968
-
969
- function estimateTokensWithProfile(text: string): number {
970
- if (activeComparisonProfile) {
971
- activeComparisonProfile.estimateTokensCalls += 1;
972
- }
973
- return estimateTokens(text);
974
- }
975
-
976
- function recordSort(length: number): void {
977
- if (!activeComparisonProfile) {
978
- return;
979
- }
980
- activeComparisonProfile.sortCalls += 1;
981
- activeComparisonProfile.totalSortedLength += length;
982
- }
983
-
984
- function elapsedMs(start: bigint): number {
985
- return Number(process.hrtime.bigint() - start) / 1_000_000;
986
- }
987
-
988
- function computePastness(text: string): {
989
- firstPersonClauseCount: number;
990
- prospectivePersonalVerbCount: number;
991
- planningDensity: number;
992
- pastness: number;
993
- } {
994
- const lower = text.toLowerCase();
995
- const firstPersonClauseCount = Math.max(1, countPatternMatches(lower, COMPARISON_FIRST_PERSON_CLAUSE_PATTERNS));
996
- const prospectivePersonalVerbCount = countPatternMatches(lower, COMPARISON_PROSPECTIVE_PERSONAL_PATTERNS);
997
- const planningDensity = prospectivePersonalVerbCount / firstPersonClauseCount;
998
- return {
999
- firstPersonClauseCount,
1000
- prospectivePersonalVerbCount,
1001
- planningDensity,
1002
- pastness: clamp01(1 - planningDensity),
1003
- };
1004
- }
1005
-
1006
- function countPatternMatches(text: string, patterns: RegExp[]): number {
1007
- let count = 0;
1008
- for (const pattern of patterns) {
1009
- for (const _match of text.matchAll(pattern)) {
1010
- count += 1;
1011
- }
1012
- }
1013
- return count;
1014
- }
1015
-
1016
- function touchTemporalAnchorCache(cacheKey: string, value: number): void {
1017
- if (temporalAnchorCache.has(cacheKey)) {
1018
- temporalAnchorCache.delete(cacheKey);
1019
- }
1020
- temporalAnchorCache.set(cacheKey, value);
1021
- if (temporalAnchorCache.size > TEMPORAL_ANCHOR_CACHE_MAX) {
1022
- const oldestKey = temporalAnchorCache.keys().next().value;
1023
- if (typeof oldestKey === "string") {
1024
- temporalAnchorCache.delete(oldestKey);
1025
- }
1026
- }
1027
- }
1028
-
1029
- function clamp01(value: number): number {
1030
- return Math.min(1, Math.max(0, value));
1031
- }