recallnest 1.4.1 → 2.2.1
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/.env +4 -0
- package/CLAUDE.md +7 -3
- package/README.md +202 -130
- package/README_CN.md +263 -190
- package/bin/recallnest.mjs +62 -0
- package/config.json +55 -0
- package/daily-recall.sh +23 -0
- package/integrations/setup-all.sh +31 -0
- package/logs/daily-recall.error.log +0 -0
- package/logs/daily-recall.log +30 -0
- package/logs/ingest-2026-04-11.log +110 -0
- package/logs/ingest-2026-04-12.log +90 -0
- package/logs/launchagent-stderr.log +0 -0
- package/logs/launchagent-stdout.log +0 -0
- package/package.json +10 -3
- package/src/access-tracker.ts +3 -2
- package/src/activity-counter.ts +109 -0
- package/src/adaptive-retrieval.ts +6 -5
- package/src/admission-control.ts +163 -0
- package/src/anchor-generator.ts +80 -0
- package/src/api-server.ts +10 -0
- package/src/audit-log.ts +122 -0
- package/src/auto-gc.ts +98 -29
- package/src/auto-recall.ts +33 -4
- package/src/capture-engine.ts +372 -23
- package/src/cascade-forget.ts +3 -2
- package/src/cli.ts +107 -5
- package/src/compat.ts +22 -0
- package/src/confidence-tracker.ts +210 -47
- package/src/conflict-store.ts +12 -3
- package/src/consolidation-engine.ts +296 -8
- package/src/context-collapse-renderer.ts +222 -0
- package/src/context-composer-stable-selection.ts +5 -1
- package/src/context-composer.ts +239 -4
- package/src/context-reconstructor.ts +404 -0
- package/src/conversation-importer.ts +441 -0
- package/src/data-checkup.ts +232 -0
- package/src/decay-engine.ts +84 -0
- package/src/distill-lock.ts +122 -0
- package/src/doctor.ts +8 -10
- package/src/dream-pipeline.ts +227 -0
- package/src/emotion-detector.ts +90 -0
- package/src/eval.ts +3 -2
- package/src/forget-engine.ts +269 -0
- package/src/frequency-tracker.ts +175 -0
- package/src/graph-export.ts +644 -0
- package/src/ingest.ts +36 -6
- package/src/interference-detector.ts +187 -0
- package/src/kg-extractor.ts +15 -24
- package/src/kg-store.ts +2 -1
- package/src/language-hook.ts +67 -0
- package/src/llm-client.ts +289 -6
- package/src/mcp-server.ts +707 -80
- package/src/memory-assets.ts +22 -5
- package/src/memory-evolution.ts +365 -0
- package/src/memory-lint.ts +429 -0
- package/src/memory-output.ts +69 -0
- package/src/memory-schema.ts +74 -2
- package/src/narrative-schema.ts +79 -0
- package/src/narrative-tagger.ts +164 -0
- package/src/pii-detector.ts +98 -0
- package/src/post-retrieval-filter.ts +76 -0
- package/src/prediction-engine.ts +253 -0
- package/src/preference-slots.ts +65 -2
- package/src/prospective-memory.ts +215 -0
- package/src/query-expander.ts +67 -0
- package/src/recall-governor.ts +183 -0
- package/src/retention-policy.ts +90 -0
- package/src/retriever.ts +828 -55
- package/src/rif.ts +34 -8
- package/src/runtime-config.ts +23 -8
- package/src/scope-inventory.ts +4 -3
- package/src/scope-policy.ts +4 -0
- package/src/session-distiller.ts +307 -0
- package/src/session-output.ts +40 -0
- package/src/session-schema.ts +37 -0
- package/src/session-store.ts +7 -2
- package/src/skill-engine.ts +208 -0
- package/src/skill-promotion.ts +293 -0
- package/src/skill-schema.ts +39 -0
- package/src/store.ts +96 -10
- package/src/topic-tag.ts +66 -0
- package/src/tracker.ts +3 -1
- package/src/ui-server.ts +80 -8
- package/src/workflow-observation-scope-review.ts +2 -1
- package/src/workflow-observation-store.ts +2 -1
- package/src/write-verifier.ts +126 -0
- package/.claude-plugin/CLAUDE.md +0 -29
- package/.claude-plugin/marketplace.json +0 -18
- package/.github/workflows/ci.yml +0 -28
- package/.mcp.json +0 -9
- package/CHANGELOG.md +0 -51
- package/DEMO.md +0 -94
- package/EVAL.md +0 -89
- package/FAILURES.md +0 -75
- package/OPERATIONS.md +0 -59
- package/ROADMAP.md +0 -198
- package/assets/recallnest-workbench.png +0 -0
- package/assets/ui/app.js +0 -807
- package/assets/ui/index.html +0 -154
- package/assets/ui/styles.css +0 -605
- package/assets/wechat-qr.jpg +0 -0
- package/daily-reflections/2026-03-05.md +0 -15
- package/daily-reflections/2026-03-06.md +0 -15
- package/daily-reflections/2026-03-07.md +0 -7
- package/daily-reflections/2026-03-08.md +0 -34
- package/daily-reflections/2026-03-09.md +0 -35
- package/daily-reflections/2026-03-10.md +0 -36
- package/daily-reflections/2026-03-11.md +0 -35
- package/daily-reflections/2026-03-12.md +0 -36
- package/daily-reflections/2026-03-13.md +0 -35
- package/daily-reflections/2026-03-14.md +0 -35
- package/daily-reflections/2026-03-15.md +0 -35
- package/daily-reflections/2026-03-16.md +0 -41
- package/daily-reflections/2026-03-17.md +0 -10
- package/daily-reflections/2026-03-18.md +0 -10
- package/daily-reflections/2026-03-19.md +0 -10
- package/daily-reflections/2026-03-20.md +0 -10
- package/daily-reflections/2026-03-21.md +0 -10
- package/daily-reflections/2026-03-22.md +0 -10
- package/daily-reflections/2026-03-23.md +0 -10
- package/daily-reflections/2026-03-24.md +0 -10
- package/daily-reflections/2026-03-25.md +0 -32
- package/daily-reflections/2026-03-26.md +0 -37
- package/daily-reflections/2026-03-27.md +0 -35
- package/daily-reflections/2026-03-28.md +0 -35
- package/daily-reflections/2026-03-29.md +0 -35
- package/daily-reflections/2026-03-30.md +0 -36
- package/daily-reflections/2026-03-31.md +0 -35
- package/daily-reflections/2026-04-01.md +0 -35
- package/docs/api-reference.md +0 -1291
- package/docs/architecture.md +0 -168
- package/docs/memory-boundary-contract.md +0 -226
- package/docs/memory-categories.md +0 -119
- package/docs/pipeline-audit-ultramemory.md +0 -177
- package/docs/plans/2026-03-08-query-expansion.md +0 -270
- package/docs/plans/2026-03-16-continuity-layer.md +0 -196
- package/docs/plans/mcp-tiering-plan.md +0 -126
- package/docs/research/recall-quality-audit-2026-03-22.md +0 -153
- package/docs/self-evolution.md +0 -198
- package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +0 -782
- package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +0 -77
- package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +0 -95
- package/eval/continuity/results-2026-03-16.md +0 -117
- package/eval/continuity/results-2026-03-19.md +0 -453
- package/eval/continuity/runbook.md +0 -110
- package/eval/continuity/seed-cases.md +0 -54
- package/eval/reports/2026-03-06-abstract-query-baseline.md +0 -96
- package/eval/reports/2026-03-06-aws-ssh-baseline.md +0 -82
- package/eval/reports/2026-03-06-baseline.md +0 -82
- package/eval/reports/query-expansion.md +0 -138
- package/lm +0 -19
- package/plan-v1.2-distributable.md +0 -105
- package/scripts/archive-distilled-junk.ts +0 -116
- package/scripts/backfill-l0l1l2.ts +0 -98
- package/scripts/blind-scan.ts +0 -588
- package/scripts/cleanup-step1-exact-dups.ts +0 -117
- package/scripts/cleanup-step2-template-dups.ts +0 -113
- package/scripts/cleanup-step3-stale-content.ts +0 -149
- package/scripts/cleanup-step4-orphan-scopes.ts +0 -131
- package/scripts/delete-distilled-junk.ts +0 -88
- package/scripts/distill-facts.ts +0 -305
- package/scripts/health-check.ts +0 -343
- package/scripts/incremental-ingest.sh +0 -71
- package/scripts/mlp-health-check.ts +0 -382
- package/scripts/pull-claude-desktop.ts +0 -515
- package/scripts/re-ingest-cc.sh +0 -66
- package/scripts/rebalance-memory-health.ts +0 -181
- package/scripts/run-desktop-ingest.sh +0 -11
- package/scripts/start-server.sh +0 -12
- package/scripts/viz-dashboard.html +0 -423
- package/scripts/viz-export.ts +0 -703
- package/src/__tests__/access-tracker-novelty.test.ts +0 -228
- package/src/__tests__/auto-consolidation.test.ts +0 -168
- package/src/__tests__/auto-recall.test.ts +0 -131
- package/src/__tests__/batch-dedup.test.ts +0 -65
- package/src/__tests__/capture-engine.test.ts +0 -1142
- package/src/__tests__/capture-heuristic.test.ts +0 -171
- package/src/__tests__/cascade-forget.test.ts +0 -210
- package/src/__tests__/category-min-scores.test.ts +0 -33
- package/src/__tests__/confidence-tracker.test.ts +0 -196
- package/src/__tests__/conflict-advisor.test.ts +0 -206
- package/src/__tests__/conflict-escalation.test.ts +0 -149
- package/src/__tests__/conflict-lifecycle.test.ts +0 -83
- package/src/__tests__/conflict-output.test.ts +0 -85
- package/src/__tests__/conflict-store.test.ts +0 -374
- package/src/__tests__/consolidation-engine.test.ts +0 -181
- package/src/__tests__/context-composer-pins.test.ts +0 -64
- package/src/__tests__/context-composer-task-selection.test.ts +0 -24
- package/src/__tests__/context-renderer.test.ts +0 -91
- package/src/__tests__/continuity-eval.test.ts +0 -275
- package/src/__tests__/continuity-schema.test.ts +0 -97
- package/src/__tests__/core-recall-eval.test.ts +0 -235
- package/src/__tests__/core-summary.test.ts +0 -109
- package/src/__tests__/dedup-actions.test.ts +0 -58
- package/src/__tests__/deterministic-id.test.ts +0 -39
- package/src/__tests__/doctor.test.ts +0 -303
- package/src/__tests__/drill-down.test.ts +0 -108
- package/src/__tests__/embedder.test.ts +0 -122
- package/src/__tests__/entity-resolver.test.ts +0 -58
- package/src/__tests__/event-segmenter.test.ts +0 -153
- package/src/__tests__/hotness-blend.test.ts +0 -76
- package/src/__tests__/ingest.test.ts +0 -360
- package/src/__tests__/kg-extractor.test.ts +0 -184
- package/src/__tests__/kg-store.test.ts +0 -198
- package/src/__tests__/llm-consolidation.test.ts +0 -226
- package/src/__tests__/memory-boundaries.test.ts +0 -237
- package/src/__tests__/memory-health-rebalance.test.ts +0 -144
- package/src/__tests__/memory-output.test.ts +0 -215
- package/src/__tests__/multi-vector.test.ts +0 -242
- package/src/__tests__/ppr-traversal.test.ts +0 -256
- package/src/__tests__/preference-guard-smoke.test.ts +0 -211
- package/src/__tests__/preference-matcher.test.ts +0 -230
- package/src/__tests__/preference-slots.test.ts +0 -106
- package/src/__tests__/prospective-memory.test.ts +0 -283
- package/src/__tests__/query-entity-detector.test.ts +0 -171
- package/src/__tests__/query-expander.test.ts +0 -34
- package/src/__tests__/recall-mode.test.ts +0 -64
- package/src/__tests__/result-synthesizer.test.ts +0 -198
- package/src/__tests__/resume-context.test.ts +0 -4743
- package/src/__tests__/retrieval-trace.test.ts +0 -213
- package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
- package/src/__tests__/retroactive-boost.test.ts +0 -223
- package/src/__tests__/scope-inventory.test.ts +0 -200
- package/src/__tests__/scope-policy.test.ts +0 -69
- package/src/__tests__/session-checkpoint.test.ts +0 -259
- package/src/__tests__/session-gc.test.ts +0 -215
- package/src/__tests__/store-scope-audit.test.ts +0 -81
- package/src/__tests__/temporal-parser.test.ts +0 -241
- package/src/__tests__/term-registry.test.ts +0 -121
- package/src/__tests__/tool-output-compressor.test.ts +0 -133
- package/src/__tests__/version-manager.test.ts +0 -213
- package/src/__tests__/workflow-observation.test.ts +0 -400
package/src/preference-slots.ts
CHANGED
|
@@ -92,10 +92,17 @@ export interface ToolChoicePreferenceSlot {
|
|
|
92
92
|
avoidedTool: string;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
export interface ImplicitUsagePreferenceSlot {
|
|
96
|
+
type: "implicit-usage";
|
|
97
|
+
/** Normalized subject: the tool, product, or activity mentioned */
|
|
98
|
+
subject: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
95
101
|
export type PreferenceSlot =
|
|
96
102
|
| AtomicBrandItemPreferenceSlot
|
|
97
103
|
| ReplyStylePreferenceSlot
|
|
98
|
-
| ToolChoicePreferenceSlot
|
|
104
|
+
| ToolChoicePreferenceSlot
|
|
105
|
+
| ImplicitUsagePreferenceSlot;
|
|
99
106
|
|
|
100
107
|
function normalizePreferenceText(value: string): string {
|
|
101
108
|
return value
|
|
@@ -227,10 +234,62 @@ export function inferToolChoicePreferenceSlot(text: string): ToolChoicePreferenc
|
|
|
227
234
|
return null;
|
|
228
235
|
}
|
|
229
236
|
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// Implicit usage preference detection
|
|
239
|
+
// Captures "I use X", "I have a X", "I enjoy X", "I'm learning X" etc.
|
|
240
|
+
// These are indirect signals — user never says "I prefer X" explicitly.
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
const IMPLICIT_USAGE_EN_PATTERNS: RegExp[] = [
|
|
244
|
+
// "I/User use(s)/using X for..." or at end of sentence
|
|
245
|
+
/\b(?:i|user)\s+(?:use[sd]?|using|am\s+using|have\s+been\s+using|work[s]?\s+with)\s+(?<subject>[A-Za-z][A-Za-z0-9\s.+#\-']{1,40}?)\s*(?=\s+(?:for|to|in|at|on|as|which|because|since)\b|[,.:;!?]|$)/i,
|
|
246
|
+
// "I/User have/own a X (camera|phone|...)" or at end
|
|
247
|
+
/\b(?:i|user)\s+(?:have|has|own[s]?|bought|got)\s+(?:a |an |the )?(?<subject>[A-Za-z][A-Za-z0-9\s.+#\-']{1,40}?)\s*(?=\s+(?:camera|phone|laptop|computer|tablet|watch|car|bike|guitar|piano|keyboard|headphones|monitor|setup|kit|device)\b|[,.:;!?]|$)/i,
|
|
248
|
+
// "I/User enjoy(s)/love(s) (to use) X" (no "from" — avoids brand-item overlap)
|
|
249
|
+
/\b(?:i|user)\s+(?:enjoy[s]?|love[s]?|really\s+like[s]?)\s+(?:to\s+(?:use\s+)?)?(?<subject>[A-Za-z][A-Za-z0-9\s.+#\-']{1,40}?)\s*(?=[,.:;!?]|$)/i,
|
|
250
|
+
// "I'm learning/studying X"
|
|
251
|
+
/\b(?:i'm|i\s+am|user\s+is)\s+(?:learning|studying|interested\s+in|passionate\s+about)\s+(?<subject>[A-Za-z][A-Za-z0-9\s.+#\-']{1,40}?)\s*(?=[,.:;!?]|$)/i,
|
|
252
|
+
];
|
|
253
|
+
|
|
254
|
+
const IMPLICIT_USAGE_CN_PATTERNS: RegExp[] = [
|
|
255
|
+
// "我用/在用 X" (lookahead stops at verb complements or punctuation; no \b for CJK)
|
|
256
|
+
/(?:我|用户)(?:在)?(?:用|使用)\s*(?<subject>[\p{Script=Han}A-Za-z0-9][\p{Script=Han}A-Za-z0-9\s.+#\-]{0,30}?)(?=\s*(?:来|做|进行|处理|开发|编辑)|[,。!?,.:;!?]|$)/u,
|
|
257
|
+
// "我有/买了 X"
|
|
258
|
+
/(?:我|用户)(?:有|买了|入了)\s*(?:一[个台部套])?(?<subject>[\p{Script=Han}A-Za-z0-9][\p{Script=Han}A-Za-z0-9\s.+#\-]{0,30}?)(?=[,。!?,.:;!?]|$)/u,
|
|
259
|
+
// "我喜欢做 X" / "我爱 X" (activity, not brand-item — no "的" connector)
|
|
260
|
+
/(?:我|用户)(?:很)?(?:喜欢|爱|享受)\s*(?:做|玩|学|用)\s*(?<subject>[\p{Script=Han}A-Za-z0-9][\p{Script=Han}A-Za-z0-9\s.+#\-]{0,30}?)(?=[,。!?,.:;!?]|$)/u,
|
|
261
|
+
// "我在学/研究 X"
|
|
262
|
+
/(?:我|用户)(?:在学|正在学|在研究)\s*(?<subject>[\p{Script=Han}A-Za-z0-9][\p{Script=Han}A-Za-z0-9\s.+#\-]{0,30}?)(?=[,。!?,.:;!?]|$)/u,
|
|
263
|
+
];
|
|
264
|
+
|
|
265
|
+
/** Minimum normalized subject length to avoid false positives like "I use it" */
|
|
266
|
+
const IMPLICIT_USAGE_MIN_SUBJECT_LEN = 2;
|
|
267
|
+
|
|
268
|
+
export function inferImplicitUsageSlot(text: string): ImplicitUsagePreferenceSlot | null {
|
|
269
|
+
const normalizedText = normalizePreferenceText(text);
|
|
270
|
+
|
|
271
|
+
for (const pattern of [...IMPLICIT_USAGE_EN_PATTERNS, ...IMPLICIT_USAGE_CN_PATTERNS]) {
|
|
272
|
+
const match = normalizedText.match(pattern);
|
|
273
|
+
if (!match?.groups?.subject) continue;
|
|
274
|
+
|
|
275
|
+
const subject = normalizePreferenceToken(match.groups.subject);
|
|
276
|
+
if (subject.length < IMPLICIT_USAGE_MIN_SUBJECT_LEN) continue;
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
type: "implicit-usage",
|
|
280
|
+
subject,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
230
287
|
export function inferPreferenceSlot(text: string): PreferenceSlot | null {
|
|
288
|
+
// Explicit patterns take priority over implicit detection
|
|
231
289
|
return inferAtomicBrandItemPreferenceSlot(text)
|
|
232
290
|
|| inferReplyStylePreferenceSlot(text)
|
|
233
|
-
|| inferToolChoicePreferenceSlot(text)
|
|
291
|
+
|| inferToolChoicePreferenceSlot(text)
|
|
292
|
+
|| inferImplicitUsageSlot(text);
|
|
234
293
|
}
|
|
235
294
|
|
|
236
295
|
export function samePreferenceSlot(
|
|
@@ -255,5 +314,9 @@ export function samePreferenceSlot(
|
|
|
255
314
|
left.avoidedTool === right.avoidedTool;
|
|
256
315
|
}
|
|
257
316
|
|
|
317
|
+
if (left.type === "implicit-usage" && right.type === "implicit-usage") {
|
|
318
|
+
return left.subject === right.subject;
|
|
319
|
+
}
|
|
320
|
+
|
|
258
321
|
return false;
|
|
259
322
|
}
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
import type { MemoryStore, MemoryEntry } from "./store.js";
|
|
16
16
|
import type { Embedder } from "./embedder.js";
|
|
17
17
|
import { logInfo } from "./stderr-log.js";
|
|
18
|
+
import type { ScoredPrediction, PredictionContext } from "./prediction-engine.js";
|
|
19
|
+
import { collectSignals, scorePredictions } from "./prediction-engine.js";
|
|
20
|
+
import { isPredictiveMemoryEnabled } from "./memory-schema.js";
|
|
18
21
|
|
|
19
22
|
// ============================================================================
|
|
20
23
|
// Types
|
|
@@ -27,6 +30,16 @@ export interface ProspectiveMetadata {
|
|
|
27
30
|
createdAt: string;
|
|
28
31
|
firedAt?: string;
|
|
29
32
|
expiresAt?: string;
|
|
33
|
+
/** HP-predictive: "explicit" = user-created, "predicted" = heuristic-generated */
|
|
34
|
+
source?: "explicit" | "predicted";
|
|
35
|
+
/** HP-predictive: confidence score 0-1 for predicted reminders */
|
|
36
|
+
confidence?: number;
|
|
37
|
+
/** HP-predictive: evidence strings explaining why this was predicted */
|
|
38
|
+
evidence?: string[];
|
|
39
|
+
/** HP-predictive: last time this prediction was surfaced to the user */
|
|
40
|
+
lastSuggestedAt?: string;
|
|
41
|
+
/** HP-predictive: when the user accepted/acted on this predicted reminder */
|
|
42
|
+
acceptedAt?: string;
|
|
30
43
|
}
|
|
31
44
|
|
|
32
45
|
export interface Reminder {
|
|
@@ -42,6 +55,20 @@ export interface SetReminderParams {
|
|
|
42
55
|
expiresInDays?: number;
|
|
43
56
|
}
|
|
44
57
|
|
|
58
|
+
export interface SuggestedReminder {
|
|
59
|
+
entryId: string;
|
|
60
|
+
trigger: string;
|
|
61
|
+
action: string;
|
|
62
|
+
confidence: number;
|
|
63
|
+
evidence: string[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Default expiry for predicted reminders: 7 days */
|
|
67
|
+
const PREDICTED_REMINDER_EXPIRY_DAYS = 7;
|
|
68
|
+
|
|
69
|
+
/** Confidence penalty per ignored suggestion cycle */
|
|
70
|
+
const IGNORE_CONFIDENCE_PENALTY = 0.15;
|
|
71
|
+
|
|
45
72
|
// ============================================================================
|
|
46
73
|
// Core Functions
|
|
47
74
|
// ============================================================================
|
|
@@ -62,6 +89,7 @@ export async function setReminder(
|
|
|
62
89
|
trigger: params.trigger,
|
|
63
90
|
action: params.action,
|
|
64
91
|
status: "pending",
|
|
92
|
+
source: "explicit",
|
|
65
93
|
createdAt: new Date().toISOString(),
|
|
66
94
|
...(params.expiresInDays
|
|
67
95
|
? { expiresAt: new Date(Date.now() + params.expiresInDays * 86_400_000).toISOString() }
|
|
@@ -168,6 +196,193 @@ export function formatReminders(reminders: Reminder[]): string[] {
|
|
|
168
196
|
);
|
|
169
197
|
}
|
|
170
198
|
|
|
199
|
+
// ============================================================================
|
|
200
|
+
// HP-predictive: Predicted Reminders
|
|
201
|
+
// ============================================================================
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Generate predicted reminders from behavioral signals.
|
|
205
|
+
* Deduplicates against existing pending reminders via vector similarity.
|
|
206
|
+
* Stores new predictions as ephemeral entries with 7-day expiry.
|
|
207
|
+
*
|
|
208
|
+
* @returns Array of suggested reminders to surface to the user
|
|
209
|
+
*/
|
|
210
|
+
export async function suggestPredictedReminders(
|
|
211
|
+
store: MemoryStore,
|
|
212
|
+
embedder: Embedder,
|
|
213
|
+
predictionContext: PredictionContext,
|
|
214
|
+
scope: string,
|
|
215
|
+
): Promise<SuggestedReminder[]> {
|
|
216
|
+
if (!isPredictiveMemoryEnabled()) return [];
|
|
217
|
+
|
|
218
|
+
// 1. Collect and score signals
|
|
219
|
+
const signals = collectSignals(predictionContext);
|
|
220
|
+
const predictions = scorePredictions(signals);
|
|
221
|
+
|
|
222
|
+
if (predictions.length === 0) return [];
|
|
223
|
+
|
|
224
|
+
const suggested: SuggestedReminder[] = [];
|
|
225
|
+
const now = predictionContext.now ?? new Date();
|
|
226
|
+
|
|
227
|
+
for (const prediction of predictions) {
|
|
228
|
+
// 2. Deduplicate: vector search for similar existing reminders
|
|
229
|
+
const isDuplicate = await isDuplicateReminder(store, embedder, prediction, [scope]);
|
|
230
|
+
if (isDuplicate) continue;
|
|
231
|
+
|
|
232
|
+
// 3. Store as predicted + ephemeral reminder with 7-day expiry
|
|
233
|
+
const text = `[Predicted Reminder] ${prediction.action}`;
|
|
234
|
+
const vector = await embedder.embedPassage(text);
|
|
235
|
+
|
|
236
|
+
const prospective: ProspectiveMetadata = {
|
|
237
|
+
trigger: prediction.trigger,
|
|
238
|
+
action: prediction.action,
|
|
239
|
+
status: "pending",
|
|
240
|
+
source: "predicted",
|
|
241
|
+
confidence: prediction.confidence,
|
|
242
|
+
evidence: prediction.evidence,
|
|
243
|
+
lastSuggestedAt: now.toISOString(),
|
|
244
|
+
createdAt: now.toISOString(),
|
|
245
|
+
expiresAt: new Date(now.getTime() + PREDICTED_REMINDER_EXPIRY_DAYS * 86_400_000).toISOString(),
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const entry = await store.store({
|
|
249
|
+
text,
|
|
250
|
+
vector,
|
|
251
|
+
category: "patterns",
|
|
252
|
+
scope,
|
|
253
|
+
importance: 0.5, // Lower than explicit reminders (0.75)
|
|
254
|
+
metadata: JSON.stringify({
|
|
255
|
+
prospective,
|
|
256
|
+
tier: "working",
|
|
257
|
+
privacyTier: "ephemeral",
|
|
258
|
+
boundary: { layer: "ephemeral", authority: "system" },
|
|
259
|
+
}),
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
suggested.push({
|
|
263
|
+
entryId: entry.id,
|
|
264
|
+
trigger: prediction.trigger,
|
|
265
|
+
action: prediction.action,
|
|
266
|
+
confidence: prediction.confidence,
|
|
267
|
+
evidence: prediction.evidence,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
logInfo(`[INFO] Predicted reminder stored: "${prediction.trigger}" (confidence: ${prediction.confidence})`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return suggested;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Accept a predicted reminder — upgrade it to explicit, persist as durable.
|
|
278
|
+
*/
|
|
279
|
+
export async function acceptPredictedReminder(
|
|
280
|
+
store: MemoryStore,
|
|
281
|
+
entryId: string,
|
|
282
|
+
scopeFilter?: string[],
|
|
283
|
+
): Promise<boolean> {
|
|
284
|
+
const entry = await store.getById(entryId);
|
|
285
|
+
if (!entry) return false;
|
|
286
|
+
|
|
287
|
+
const meta = parseMetadata(entry.metadata);
|
|
288
|
+
const prospective = meta.prospective as ProspectiveMetadata | undefined;
|
|
289
|
+
if (!prospective || prospective.source !== "predicted" || prospective.status !== "pending") return false;
|
|
290
|
+
|
|
291
|
+
// Upgrade: predicted → explicit, ephemeral → durable
|
|
292
|
+
prospective.source = "explicit";
|
|
293
|
+
prospective.acceptedAt = new Date().toISOString();
|
|
294
|
+
meta.prospective = prospective;
|
|
295
|
+
meta.privacyTier = "durable";
|
|
296
|
+
if (meta.boundary && typeof meta.boundary === "object") {
|
|
297
|
+
(meta.boundary as Record<string, string>).layer = "durable";
|
|
298
|
+
(meta.boundary as Record<string, string>).authority = "user";
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
await store.update(entryId, {
|
|
302
|
+
metadata: JSON.stringify(meta),
|
|
303
|
+
importance: 0.75, // Upgrade to explicit-level importance
|
|
304
|
+
}, scopeFilter);
|
|
305
|
+
|
|
306
|
+
logInfo(`[INFO] Predicted reminder accepted and promoted: ${entryId}`);
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Demote a predicted reminder — reduce confidence.
|
|
312
|
+
* If confidence drops below threshold after penalty, mark as expired.
|
|
313
|
+
*/
|
|
314
|
+
export async function demotePredictedReminder(
|
|
315
|
+
store: MemoryStore,
|
|
316
|
+
entryId: string,
|
|
317
|
+
scopeFilter?: string[],
|
|
318
|
+
): Promise<boolean> {
|
|
319
|
+
const entry = await store.getById(entryId);
|
|
320
|
+
if (!entry) return false;
|
|
321
|
+
|
|
322
|
+
const meta = parseMetadata(entry.metadata);
|
|
323
|
+
const prospective = meta.prospective as ProspectiveMetadata | undefined;
|
|
324
|
+
if (!prospective || prospective.source !== "predicted" || prospective.status !== "pending") return false;
|
|
325
|
+
|
|
326
|
+
const newConfidence = Math.max(0, (prospective.confidence ?? 0) - IGNORE_CONFIDENCE_PENALTY);
|
|
327
|
+
|
|
328
|
+
if (newConfidence < 0.6) {
|
|
329
|
+
// Below threshold — expire it
|
|
330
|
+
prospective.status = "expired";
|
|
331
|
+
prospective.confidence = newConfidence;
|
|
332
|
+
meta.prospective = prospective;
|
|
333
|
+
await store.update(entryId, { metadata: JSON.stringify(meta) }, scopeFilter);
|
|
334
|
+
logInfo(`[INFO] Predicted reminder demoted below threshold and expired: ${entryId}`);
|
|
335
|
+
} else {
|
|
336
|
+
// Still above threshold — just lower confidence
|
|
337
|
+
prospective.confidence = newConfidence;
|
|
338
|
+
prospective.lastSuggestedAt = new Date().toISOString();
|
|
339
|
+
meta.prospective = prospective;
|
|
340
|
+
await store.update(entryId, { metadata: JSON.stringify(meta) }, scopeFilter);
|
|
341
|
+
logInfo(`[INFO] Predicted reminder demoted: ${entryId} (new confidence: ${newConfidence})`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Format suggested reminders for output in search_memory results.
|
|
349
|
+
*/
|
|
350
|
+
export function formatSuggestedReminders(suggestions: SuggestedReminder[]): string {
|
|
351
|
+
if (suggestions.length === 0) return "";
|
|
352
|
+
|
|
353
|
+
const lines = suggestions.map(s => {
|
|
354
|
+
const confidenceLabel = s.confidence >= 0.8 ? "high" : s.confidence >= 0.7 ? "medium" : "low";
|
|
355
|
+
return `- [${confidenceLabel}] ${s.action}\n Evidence: ${s.evidence.join("; ")}`;
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
return "\n\n--- Suggested Reminders ---\n" + lines.join("\n");
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Check if a prediction duplicates an existing pending reminder.
|
|
363
|
+
* Uses vector similarity — threshold 0.75 = likely duplicate.
|
|
364
|
+
*/
|
|
365
|
+
async function isDuplicateReminder(
|
|
366
|
+
store: MemoryStore,
|
|
367
|
+
embedder: Embedder,
|
|
368
|
+
prediction: ScoredPrediction,
|
|
369
|
+
scopeFilter: string[],
|
|
370
|
+
): Promise<boolean> {
|
|
371
|
+
const searchText = `${prediction.trigger} ${prediction.action}`;
|
|
372
|
+
const vector = await embedder.embedPassage(searchText);
|
|
373
|
+
const candidates = await store.vectorSearch(vector, 5, 0.75, scopeFilter);
|
|
374
|
+
|
|
375
|
+
for (const candidate of candidates) {
|
|
376
|
+
const meta = parseMetadata(candidate.entry.metadata);
|
|
377
|
+
const prospective = meta.prospective as ProspectiveMetadata | undefined;
|
|
378
|
+
if (prospective && prospective.status === "pending") {
|
|
379
|
+
return true; // Found an existing pending reminder that's similar enough
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
|
|
171
386
|
// ============================================================================
|
|
172
387
|
// Helpers
|
|
173
388
|
// ============================================================================
|
package/src/query-expander.ts
CHANGED
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
* MAX_EXPANSION_TERMS cap to prevent query bloat hurting BM25 precision.
|
|
8
8
|
* Structured synonym entries separating CN (substring) from EN (word-boundary).
|
|
9
9
|
* v1.2: Entity resolution pre-pass normalizes aliases before synonym expansion.
|
|
10
|
+
* v1.3: P0.3 — User-level alias-map (data/alias-map.json) for short query expansion.
|
|
10
11
|
*/
|
|
11
12
|
|
|
13
|
+
import { existsSync, readFileSync } from "fs";
|
|
14
|
+
import { join } from "path";
|
|
12
15
|
import { resolveQueryEntities } from "./entity-resolver.js";
|
|
13
16
|
|
|
14
17
|
interface SynonymEntry {
|
|
@@ -59,6 +62,56 @@ const SYNONYM_MAP: SynonymEntry[] = [
|
|
|
59
62
|
/** Maximum expansion terms to prevent query bloat hurting BM25 precision */
|
|
60
63
|
const MAX_EXPANSION_TERMS = 5;
|
|
61
64
|
|
|
65
|
+
// ============================================================================
|
|
66
|
+
// P0.3: User-level alias-map for short query expansion
|
|
67
|
+
// ============================================================================
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Alias map entry: a short trigger → expansion terms.
|
|
71
|
+
* Loaded from data/alias-map.json on first use (lazy, cached).
|
|
72
|
+
*/
|
|
73
|
+
export interface AliasEntry {
|
|
74
|
+
trigger: string;
|
|
75
|
+
expansions: string[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let aliasMapCache: AliasEntry[] | null = null;
|
|
79
|
+
let aliasMapPath: string | null = null;
|
|
80
|
+
|
|
81
|
+
/** Override the alias-map file path (for testing). */
|
|
82
|
+
export function setAliasMapPath(path: string): void {
|
|
83
|
+
aliasMapPath = path;
|
|
84
|
+
aliasMapCache = null; // force reload
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Reset alias-map cache (for testing). */
|
|
88
|
+
export function resetAliasMapCache(): void {
|
|
89
|
+
aliasMapCache = null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function loadAliasMap(): AliasEntry[] {
|
|
93
|
+
if (aliasMapCache !== null) return aliasMapCache;
|
|
94
|
+
const filePath = aliasMapPath ?? join(process.cwd(), "data", "alias-map.json");
|
|
95
|
+
try {
|
|
96
|
+
if (existsSync(filePath)) {
|
|
97
|
+
const raw = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
98
|
+
if (Array.isArray(raw)) {
|
|
99
|
+
aliasMapCache = raw.filter(
|
|
100
|
+
(e: unknown): e is AliasEntry =>
|
|
101
|
+
typeof e === "object" && e !== null &&
|
|
102
|
+
typeof (e as AliasEntry).trigger === "string" &&
|
|
103
|
+
Array.isArray((e as AliasEntry).expansions),
|
|
104
|
+
);
|
|
105
|
+
return aliasMapCache;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
// Silent — alias map is optional
|
|
110
|
+
}
|
|
111
|
+
aliasMapCache = [];
|
|
112
|
+
return aliasMapCache;
|
|
113
|
+
}
|
|
114
|
+
|
|
62
115
|
/**
|
|
63
116
|
* Expand a query by appending synonym terms from the dictionary.
|
|
64
117
|
* Returns the original query with additional terms appended.
|
|
@@ -72,6 +125,7 @@ export function expandQuery(query: string): string {
|
|
|
72
125
|
const lower = resolved.toLowerCase();
|
|
73
126
|
const additions = new Set<string>();
|
|
74
127
|
|
|
128
|
+
// Static synonym map (built-in)
|
|
75
129
|
for (const entry of SYNONYM_MAP) {
|
|
76
130
|
// Chinese triggers: substring match (CJK has no word boundaries)
|
|
77
131
|
const cnMatch = entry.cn.some(t => lower.includes(t.toLowerCase()));
|
|
@@ -90,6 +144,19 @@ export function expandQuery(query: string): string {
|
|
|
90
144
|
}
|
|
91
145
|
}
|
|
92
146
|
|
|
147
|
+
// P0.3: User alias-map (data/alias-map.json) — short triggers → expansions
|
|
148
|
+
const aliasMap = loadAliasMap();
|
|
149
|
+
for (const alias of aliasMap) {
|
|
150
|
+
const triggerLower = alias.trigger.toLowerCase();
|
|
151
|
+
if (lower.includes(triggerLower)) {
|
|
152
|
+
for (const exp of alias.expansions) {
|
|
153
|
+
if (!lower.includes(exp.toLowerCase())) {
|
|
154
|
+
additions.add(exp);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
93
160
|
if (additions.size === 0) return resolved;
|
|
94
161
|
|
|
95
162
|
// Cap expansion terms to prevent query bloat
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LME-7: Recall Governor — auto-recall 治理层
|
|
3
|
+
*
|
|
4
|
+
* 在 HybridRetriever 返回结果后、auto-recall 返回给调用方之前,
|
|
5
|
+
* 对检索结果施加 4 层治理:
|
|
6
|
+
* 1. Query 截断 — 超长 query 截到 maxQueryChars(省 embedding 成本)
|
|
7
|
+
* 2. 状态过滤 — 排除 archived/superseded 记忆(evolution 兜底)
|
|
8
|
+
* 3. 预算控制 — 总字符上限 + 条目数上限
|
|
9
|
+
* 4. 会话去重 — 同一 session 内不重复注入同一条记忆
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { isActiveMemory } from "./memory-evolution.js";
|
|
13
|
+
import type { RetrievalResult } from "./retriever.js";
|
|
14
|
+
import { logInfo } from "./stderr-log.js";
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Configuration
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
export interface GovernorConfig {
|
|
21
|
+
/** Max query length before truncation (default: 1000) */
|
|
22
|
+
maxQueryChars: number;
|
|
23
|
+
/** Max total characters across all injected memories (default: 8000) */
|
|
24
|
+
charBudget: number;
|
|
25
|
+
/** Max number of memories to inject (default: 10) */
|
|
26
|
+
maxItems: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const DEFAULT_CONFIG: GovernorConfig = {
|
|
30
|
+
maxQueryChars: 1000,
|
|
31
|
+
charBudget: 8000,
|
|
32
|
+
maxItems: 10,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function resolveGovernorConfig(
|
|
36
|
+
overrides?: Partial<GovernorConfig>,
|
|
37
|
+
): GovernorConfig {
|
|
38
|
+
return { ...DEFAULT_CONFIG, ...overrides };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Governor Session — tracks per-session dedup state
|
|
43
|
+
// ============================================================================
|
|
44
|
+
|
|
45
|
+
export class GovernorSession {
|
|
46
|
+
private readonly injectedIds = new Set<string>();
|
|
47
|
+
|
|
48
|
+
/** Mark a memory ID as already injected in this session. */
|
|
49
|
+
markInjected(id: string): void {
|
|
50
|
+
this.injectedIds.add(id);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Check whether a memory ID was already injected. */
|
|
54
|
+
wasInjected(id: string): boolean {
|
|
55
|
+
return this.injectedIds.has(id);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Bulk-mark results after governing. */
|
|
59
|
+
markAll(results: RetrievalResult[]): void {
|
|
60
|
+
for (const r of results) {
|
|
61
|
+
this.injectedIds.add(r.entry.id);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Number of unique IDs tracked so far. */
|
|
66
|
+
get size(): number {
|
|
67
|
+
return this.injectedIds.size;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// Layer 1: Query Truncation
|
|
73
|
+
// ============================================================================
|
|
74
|
+
|
|
75
|
+
export function truncateQuery(
|
|
76
|
+
query: string,
|
|
77
|
+
maxChars: number,
|
|
78
|
+
): string {
|
|
79
|
+
if (query.length <= maxChars) return query;
|
|
80
|
+
logInfo(`[recall-governor] query truncated: ${query.length} → ${maxChars} chars`);
|
|
81
|
+
return query.slice(0, maxChars);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// Layer 2: Evolution State Filter
|
|
86
|
+
// ============================================================================
|
|
87
|
+
|
|
88
|
+
function filterByEvolutionState(results: RetrievalResult[]): RetrievalResult[] {
|
|
89
|
+
const before = results.length;
|
|
90
|
+
const filtered = results.filter((r) => isActiveMemory(r.entry.metadata));
|
|
91
|
+
const dropped = before - filtered.length;
|
|
92
|
+
if (dropped > 0) {
|
|
93
|
+
logInfo(`[recall-governor] evolution filter dropped ${dropped}/${before} inactive memories`);
|
|
94
|
+
}
|
|
95
|
+
return filtered;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ============================================================================
|
|
99
|
+
// Layer 3: Budget Control
|
|
100
|
+
// ============================================================================
|
|
101
|
+
|
|
102
|
+
function applyBudget(
|
|
103
|
+
results: RetrievalResult[],
|
|
104
|
+
config: GovernorConfig,
|
|
105
|
+
): RetrievalResult[] {
|
|
106
|
+
const kept: RetrievalResult[] = [];
|
|
107
|
+
let totalChars = 0;
|
|
108
|
+
|
|
109
|
+
for (const r of results) {
|
|
110
|
+
if (kept.length >= config.maxItems) break;
|
|
111
|
+
const textLen = r.entry.text.length;
|
|
112
|
+
if (totalChars + textLen > config.charBudget && kept.length > 0) break;
|
|
113
|
+
// Always allow at least one result even if it exceeds budget
|
|
114
|
+
kept.push(r);
|
|
115
|
+
totalChars += textLen;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (kept.length < results.length) {
|
|
119
|
+
logInfo(
|
|
120
|
+
`[recall-governor] budget control: ${kept.length}/${results.length} kept ` +
|
|
121
|
+
`(${totalChars} chars, limit ${config.charBudget})`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
return kept;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ============================================================================
|
|
128
|
+
// Layer 4: Session Dedup
|
|
129
|
+
// ============================================================================
|
|
130
|
+
|
|
131
|
+
function deduplicateBySession(
|
|
132
|
+
results: RetrievalResult[],
|
|
133
|
+
session: GovernorSession | undefined,
|
|
134
|
+
): RetrievalResult[] {
|
|
135
|
+
if (!session) return results;
|
|
136
|
+
|
|
137
|
+
const before = results.length;
|
|
138
|
+
const deduped = results.filter((r) => !session.wasInjected(r.entry.id));
|
|
139
|
+
const dropped = before - deduped.length;
|
|
140
|
+
if (dropped > 0) {
|
|
141
|
+
logInfo(`[recall-governor] session dedup dropped ${dropped}/${before} already-injected memories`);
|
|
142
|
+
}
|
|
143
|
+
return deduped;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ============================================================================
|
|
147
|
+
// Public API — compose all layers
|
|
148
|
+
// ============================================================================
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Run the full governance pipeline on retrieval results.
|
|
152
|
+
* Call this after tier-aware filtering in auto-recall, before returning.
|
|
153
|
+
*
|
|
154
|
+
* @param results - Pre-sorted retrieval results (score descending)
|
|
155
|
+
* @param session - Optional GovernorSession for cross-call dedup
|
|
156
|
+
* @param config - Optional config overrides
|
|
157
|
+
* @returns Governed results, ready for injection
|
|
158
|
+
*/
|
|
159
|
+
export function governResults(
|
|
160
|
+
results: RetrievalResult[],
|
|
161
|
+
session?: GovernorSession,
|
|
162
|
+
config?: Partial<GovernorConfig>,
|
|
163
|
+
): RetrievalResult[] {
|
|
164
|
+
if (results.length === 0) return results;
|
|
165
|
+
|
|
166
|
+
const cfg = resolveGovernorConfig(config);
|
|
167
|
+
|
|
168
|
+
// Layer 2: evolution state filter
|
|
169
|
+
let governed = filterByEvolutionState(results);
|
|
170
|
+
|
|
171
|
+
// Layer 3: session dedup (before budget so deduped items don't waste budget slots)
|
|
172
|
+
governed = deduplicateBySession(governed, session);
|
|
173
|
+
|
|
174
|
+
// Layer 4: budget control
|
|
175
|
+
governed = applyBudget(governed, cfg);
|
|
176
|
+
|
|
177
|
+
// Mark newly injected IDs for future dedup
|
|
178
|
+
if (session) {
|
|
179
|
+
session.markAll(governed);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return governed;
|
|
183
|
+
}
|