recallnest 1.4.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/.claude-plugin/CLAUDE.md +29 -0
- package/.claude-plugin/marketplace.json +18 -0
- package/.env.example +3 -0
- package/.github/workflows/ci.yml +28 -0
- package/.mcp.json +9 -0
- package/CHANGELOG.md +51 -0
- package/CLAUDE.md +42 -0
- package/DEMO.md +94 -0
- package/EVAL.md +89 -0
- package/FAILURES.md +75 -0
- package/LICENSE +21 -0
- package/OPERATIONS.md +59 -0
- package/README.md +427 -0
- package/README_CN.md +425 -0
- package/ROADMAP.md +198 -0
- package/assets/recallnest-workbench.png +0 -0
- package/assets/ui/app.js +807 -0
- package/assets/ui/index.html +154 -0
- package/assets/ui/styles.css +605 -0
- package/assets/wechat-qr.jpg +0 -0
- package/config.json.example +38 -0
- package/daily-recall.sh +87 -0
- package/daily-reflections/2026-03-05.md +15 -0
- package/daily-reflections/2026-03-06.md +15 -0
- package/daily-reflections/2026-03-07.md +7 -0
- package/daily-reflections/2026-03-08.md +34 -0
- package/daily-reflections/2026-03-09.md +35 -0
- package/daily-reflections/2026-03-10.md +36 -0
- package/daily-reflections/2026-03-11.md +35 -0
- package/daily-reflections/2026-03-12.md +36 -0
- package/daily-reflections/2026-03-13.md +35 -0
- package/daily-reflections/2026-03-14.md +35 -0
- package/daily-reflections/2026-03-15.md +35 -0
- package/daily-reflections/2026-03-16.md +41 -0
- package/daily-reflections/2026-03-17.md +10 -0
- package/daily-reflections/2026-03-18.md +10 -0
- package/daily-reflections/2026-03-19.md +10 -0
- package/daily-reflections/2026-03-20.md +10 -0
- package/daily-reflections/2026-03-21.md +10 -0
- package/daily-reflections/2026-03-22.md +10 -0
- package/daily-reflections/2026-03-23.md +10 -0
- package/daily-reflections/2026-03-24.md +10 -0
- package/daily-reflections/2026-03-25.md +32 -0
- package/daily-reflections/2026-03-26.md +37 -0
- package/daily-reflections/2026-03-27.md +35 -0
- package/daily-reflections/2026-03-28.md +35 -0
- package/daily-reflections/2026-03-29.md +35 -0
- package/daily-reflections/2026-03-30.md +36 -0
- package/daily-reflections/2026-03-31.md +35 -0
- package/daily-reflections/2026-04-01.md +35 -0
- package/docs/api-reference.md +1291 -0
- package/docs/architecture.md +168 -0
- package/docs/memory-boundary-contract.md +226 -0
- package/docs/memory-categories.md +119 -0
- package/docs/pipeline-audit-ultramemory.md +177 -0
- package/docs/plans/2026-03-08-query-expansion.md +270 -0
- package/docs/plans/2026-03-16-continuity-layer.md +196 -0
- package/docs/plans/mcp-tiering-plan.md +126 -0
- package/docs/research/recall-quality-audit-2026-03-22.md +153 -0
- package/docs/self-evolution.md +198 -0
- package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +782 -0
- package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +77 -0
- package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +95 -0
- package/eval/cases.json +85 -0
- package/eval/continuity/case-seeds.json +90 -0
- package/eval/continuity/cases.json +302 -0
- package/eval/continuity/memory-seeds.json +26 -0
- package/eval/continuity/pattern-seeds.json +100 -0
- package/eval/continuity/results-2026-03-16.md +117 -0
- package/eval/continuity/results-2026-03-19.md +453 -0
- package/eval/continuity/runbook.md +110 -0
- package/eval/continuity/seed-cases.md +54 -0
- package/eval/reports/2026-03-06-abstract-query-baseline.md +96 -0
- package/eval/reports/2026-03-06-aws-ssh-baseline.md +82 -0
- package/eval/reports/2026-03-06-baseline.md +82 -0
- package/eval/reports/query-expansion.md +138 -0
- package/integrations/README.md +57 -0
- package/integrations/claude-code/README.md +133 -0
- package/integrations/claude-code/claude-md-snippet.md +31 -0
- package/integrations/claude-code/setup.sh +66 -0
- package/integrations/claude-code/smoke-continuity.sh +452 -0
- package/integrations/codex/README.md +49 -0
- package/integrations/codex/agents-md-snippet.md +31 -0
- package/integrations/codex/setup.sh +58 -0
- package/integrations/examples/claude-agent-sdk/memory-agent.ts +182 -0
- package/integrations/examples/langchain/memory-chain.py +111 -0
- package/integrations/examples/openai-agents-sdk/memory-agent.py +109 -0
- package/integrations/gemini-cli/README.md +57 -0
- package/integrations/gemini-cli/gemini-md-snippet.md +31 -0
- package/integrations/gemini-cli/setup.sh +64 -0
- package/integrations/shared/install-managed-block.sh +51 -0
- package/lm +19 -0
- package/package.json +50 -0
- package/plan-v1.2-distributable.md +105 -0
- package/scripts/archive-distilled-junk.ts +116 -0
- package/scripts/backfill-l0l1l2.ts +98 -0
- package/scripts/blind-scan.ts +588 -0
- package/scripts/cleanup-step1-exact-dups.ts +117 -0
- package/scripts/cleanup-step2-template-dups.ts +113 -0
- package/scripts/cleanup-step3-stale-content.ts +149 -0
- package/scripts/cleanup-step4-orphan-scopes.ts +131 -0
- package/scripts/delete-distilled-junk.ts +88 -0
- package/scripts/distill-facts.ts +305 -0
- package/scripts/health-check.ts +343 -0
- package/scripts/incremental-ingest.sh +71 -0
- package/scripts/mlp-health-check.ts +382 -0
- package/scripts/pull-claude-desktop.ts +515 -0
- package/scripts/re-ingest-cc.sh +66 -0
- package/scripts/rebalance-memory-health.ts +181 -0
- package/scripts/run-desktop-ingest.sh +11 -0
- package/scripts/start-server.sh +12 -0
- package/scripts/viz-dashboard.html +423 -0
- package/scripts/viz-export.ts +703 -0
- package/src/__tests__/access-tracker-novelty.test.ts +228 -0
- package/src/__tests__/auto-consolidation.test.ts +168 -0
- package/src/__tests__/auto-recall.test.ts +131 -0
- package/src/__tests__/batch-dedup.test.ts +65 -0
- package/src/__tests__/capture-engine.test.ts +1142 -0
- package/src/__tests__/capture-heuristic.test.ts +171 -0
- package/src/__tests__/cascade-forget.test.ts +210 -0
- package/src/__tests__/category-min-scores.test.ts +33 -0
- package/src/__tests__/confidence-tracker.test.ts +196 -0
- package/src/__tests__/conflict-advisor.test.ts +206 -0
- package/src/__tests__/conflict-escalation.test.ts +149 -0
- package/src/__tests__/conflict-lifecycle.test.ts +83 -0
- package/src/__tests__/conflict-output.test.ts +85 -0
- package/src/__tests__/conflict-store.test.ts +374 -0
- package/src/__tests__/consolidation-engine.test.ts +181 -0
- package/src/__tests__/context-composer-pins.test.ts +64 -0
- package/src/__tests__/context-composer-task-selection.test.ts +24 -0
- package/src/__tests__/context-renderer.test.ts +91 -0
- package/src/__tests__/continuity-eval.test.ts +275 -0
- package/src/__tests__/continuity-schema.test.ts +97 -0
- package/src/__tests__/core-recall-eval.test.ts +235 -0
- package/src/__tests__/core-summary.test.ts +109 -0
- package/src/__tests__/dedup-actions.test.ts +58 -0
- package/src/__tests__/deterministic-id.test.ts +39 -0
- package/src/__tests__/doctor.test.ts +303 -0
- package/src/__tests__/drill-down.test.ts +108 -0
- package/src/__tests__/embedder.test.ts +122 -0
- package/src/__tests__/entity-resolver.test.ts +58 -0
- package/src/__tests__/event-segmenter.test.ts +153 -0
- package/src/__tests__/hotness-blend.test.ts +76 -0
- package/src/__tests__/ingest.test.ts +360 -0
- package/src/__tests__/kg-extractor.test.ts +184 -0
- package/src/__tests__/kg-store.test.ts +198 -0
- package/src/__tests__/llm-consolidation.test.ts +226 -0
- package/src/__tests__/memory-boundaries.test.ts +237 -0
- package/src/__tests__/memory-health-rebalance.test.ts +144 -0
- package/src/__tests__/memory-output.test.ts +215 -0
- package/src/__tests__/multi-vector.test.ts +242 -0
- package/src/__tests__/ppr-traversal.test.ts +256 -0
- package/src/__tests__/preference-guard-smoke.test.ts +211 -0
- package/src/__tests__/preference-matcher.test.ts +230 -0
- package/src/__tests__/preference-slots.test.ts +106 -0
- package/src/__tests__/prospective-memory.test.ts +283 -0
- package/src/__tests__/query-entity-detector.test.ts +171 -0
- package/src/__tests__/query-expander.test.ts +34 -0
- package/src/__tests__/recall-mode.test.ts +64 -0
- package/src/__tests__/result-synthesizer.test.ts +198 -0
- package/src/__tests__/resume-context.test.ts +4743 -0
- package/src/__tests__/retrieval-trace.test.ts +213 -0
- package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
- package/src/__tests__/retroactive-boost.test.ts +223 -0
- package/src/__tests__/scope-inventory.test.ts +200 -0
- package/src/__tests__/scope-policy.test.ts +69 -0
- package/src/__tests__/session-checkpoint.test.ts +259 -0
- package/src/__tests__/session-gc.test.ts +215 -0
- package/src/__tests__/store-scope-audit.test.ts +81 -0
- package/src/__tests__/temporal-parser.test.ts +241 -0
- package/src/__tests__/term-registry.test.ts +121 -0
- package/src/__tests__/tool-output-compressor.test.ts +133 -0
- package/src/__tests__/version-manager.test.ts +213 -0
- package/src/__tests__/workflow-observation.test.ts +400 -0
- package/src/access-tracker.ts +316 -0
- package/src/adaptive-retrieval.ts +64 -0
- package/src/api-server.ts +667 -0
- package/src/asset-sync.ts +79 -0
- package/src/auto-consolidation.ts +133 -0
- package/src/auto-gc.ts +124 -0
- package/src/auto-recall.ts +95 -0
- package/src/capture-engine.ts +931 -0
- package/src/capture-heuristic.ts +177 -0
- package/src/cascade-forget.ts +135 -0
- package/src/chunker.ts +253 -0
- package/src/cli.ts +1917 -0
- package/src/confidence-tracker.ts +166 -0
- package/src/conflict-advisor.ts +433 -0
- package/src/conflict-engine.ts +181 -0
- package/src/conflict-escalation.ts +138 -0
- package/src/conflict-lifecycle.ts +67 -0
- package/src/conflict-output.ts +218 -0
- package/src/conflict-schema.ts +130 -0
- package/src/conflict-store.ts +127 -0
- package/src/consolidation-engine.ts +256 -0
- package/src/context-composer-pins.ts +183 -0
- package/src/context-composer-queries.ts +121 -0
- package/src/context-composer-scope.ts +56 -0
- package/src/context-composer-stable-selection.ts +200 -0
- package/src/context-composer-stable.ts +217 -0
- package/src/context-composer-task-fallbacks.ts +36 -0
- package/src/context-composer-task-ranking.ts +197 -0
- package/src/context-composer-task-results.ts +137 -0
- package/src/context-composer-task-selection.ts +342 -0
- package/src/context-composer-text.ts +57 -0
- package/src/context-composer.ts +300 -0
- package/src/context-renderer.ts +179 -0
- package/src/decay-engine.ts +189 -0
- package/src/doctor.ts +543 -0
- package/src/embedder.ts +557 -0
- package/src/entity-resolver.ts +131 -0
- package/src/eval.ts +632 -0
- package/src/event-segmenter.ts +216 -0
- package/src/ingest.ts +1822 -0
- package/src/kg-extractor.ts +217 -0
- package/src/kg-store.ts +353 -0
- package/src/legacy-scope-review.ts +53 -0
- package/src/llm-client.ts +629 -0
- package/src/llm-consolidation.ts +168 -0
- package/src/mcp-server.ts +1337 -0
- package/src/memory-assets.ts +475 -0
- package/src/memory-boundaries.ts +354 -0
- package/src/memory-health-rebalance.ts +220 -0
- package/src/memory-output.ts +474 -0
- package/src/memory-schema.ts +184 -0
- package/src/multi-vector.ts +234 -0
- package/src/noise-filter.ts +155 -0
- package/src/ppr-traversal.ts +279 -0
- package/src/preference-matcher.ts +174 -0
- package/src/preference-slots.ts +259 -0
- package/src/prospective-memory.ts +198 -0
- package/src/query-entity-detector.ts +207 -0
- package/src/query-expander.ts +98 -0
- package/src/result-synthesizer.ts +114 -0
- package/src/retrieval-profiles.ts +101 -0
- package/src/retrieval-trace.ts +94 -0
- package/src/retriever.ts +1338 -0
- package/src/retroactive-boost.ts +154 -0
- package/src/rif.ts +73 -0
- package/src/runtime-config.ts +172 -0
- package/src/schema-utils.ts +56 -0
- package/src/scope-inventory.ts +310 -0
- package/src/scope-policy.ts +142 -0
- package/src/session-engine.ts +168 -0
- package/src/session-output.ts +96 -0
- package/src/session-schema.ts +60 -0
- package/src/session-store.ts +293 -0
- package/src/stderr-log.ts +16 -0
- package/src/store.ts +903 -0
- package/src/temporal-parser.ts +318 -0
- package/src/term-registry.ts +791 -0
- package/src/tool-output-compressor.ts +115 -0
- package/src/tracker.ts +57 -0
- package/src/ui-server.ts +430 -0
- package/src/version-manager.ts +172 -0
- package/src/workflow-observation-engine.ts +316 -0
- package/src/workflow-observation-managed.ts +83 -0
- package/src/workflow-observation-output.ts +86 -0
- package/src/workflow-observation-schema.ts +87 -0
- package/src/workflow-observation-scope-review.ts +409 -0
- package/src/workflow-observation-store.ts +69 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Access Tracker — "用进废退" reinforcement for memory decay.
|
|
3
|
+
*
|
|
4
|
+
* Tracks how often memories are recalled and extends their effective
|
|
5
|
+
* time-decay half-life accordingly. Frequently accessed memories
|
|
6
|
+
* decay slower; rarely accessed ones decay at the base rate.
|
|
7
|
+
*
|
|
8
|
+
* v1.1: Added tier promotion/demotion on flush (borrowed from
|
|
9
|
+
* memory-lancedb-pro v1.1.0 tier-manager).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { MemoryStore } from "./store.js";
|
|
13
|
+
import { evaluateTierChange, resolveTier, type MemoryTier } from "./decay-engine.js";
|
|
14
|
+
import { logInfo, logWarn } from "./stderr-log.js";
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Configuration
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
export interface AccessTrackerConfig {
|
|
21
|
+
/** Debounce interval for flushing access counts to store (ms). Default: 5000 */
|
|
22
|
+
flushIntervalMs: number;
|
|
23
|
+
/** Reinforcement factor: how much access extends half-life. Default: 0.5 */
|
|
24
|
+
reinforcementFactor: number;
|
|
25
|
+
/** Maximum multiplier for half-life extension. Default: 3.0 */
|
|
26
|
+
maxMultiplier: number;
|
|
27
|
+
/** Half-life for access count freshness decay (days). Default: 30 */
|
|
28
|
+
accessFreshnessHalfLifeDays: number;
|
|
29
|
+
/** Novelty threshold: only reinforce when query-result distance > this (default: 0.35).
|
|
30
|
+
* Higher = stricter, fewer reinforcements. 0 = disabled (always reinforce). */
|
|
31
|
+
noveltyThreshold: number;
|
|
32
|
+
/** Cooldown period per entry ID in ms. Same entry won't be reinforced twice within this window (default: 300000 = 5 min). */
|
|
33
|
+
cooldownMs: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const DEFAULT_ACCESS_TRACKER_CONFIG: AccessTrackerConfig = {
|
|
37
|
+
flushIntervalMs: 5000,
|
|
38
|
+
reinforcementFactor: 0.5,
|
|
39
|
+
maxMultiplier: 3.0,
|
|
40
|
+
accessFreshnessHalfLifeDays: 30,
|
|
41
|
+
noveltyThreshold: 0.35,
|
|
42
|
+
cooldownMs: 300_000, // 5 minutes
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// ============================================================================
|
|
46
|
+
// Core
|
|
47
|
+
// ============================================================================
|
|
48
|
+
|
|
49
|
+
export class AccessTracker {
|
|
50
|
+
private pending = new Map<string, number>();
|
|
51
|
+
private flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
52
|
+
private flushPromise: Promise<void> | null = null;
|
|
53
|
+
/** Cooldown map: entryId → last reinforced timestamp */
|
|
54
|
+
private cooldownMap = new Map<string, number>();
|
|
55
|
+
|
|
56
|
+
constructor(
|
|
57
|
+
private store: MemoryStore,
|
|
58
|
+
private config: AccessTrackerConfig = DEFAULT_ACCESS_TRACKER_CONFIG,
|
|
59
|
+
) {}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Determine whether a retrieval result should trigger access reinforcement.
|
|
63
|
+
* Gated by two conditions:
|
|
64
|
+
* 1. Novelty: the result's similarity score must indicate sufficient distance
|
|
65
|
+
* (score ≤ 1 - noveltyThreshold) — i.e. not a near-exact repeat of the query.
|
|
66
|
+
* Lower similarity = higher novelty = worth reinforcing.
|
|
67
|
+
* 2. Cooldown: the same entry ID must not have been reinforced within cooldownMs.
|
|
68
|
+
*
|
|
69
|
+
* @param entryId The memory entry ID
|
|
70
|
+
* @param similarityScore Cosine similarity between query and result (0-1)
|
|
71
|
+
* @returns true if this entry should be reinforced
|
|
72
|
+
*/
|
|
73
|
+
shouldReinforce(entryId: string, similarityScore: number): boolean {
|
|
74
|
+
const { noveltyThreshold, cooldownMs } = this.config;
|
|
75
|
+
|
|
76
|
+
// Gate 1: Novelty — only reinforce if the retrieval is "surprising" enough.
|
|
77
|
+
// A very high similarity means the user is asking nearly the same thing as
|
|
78
|
+
// what's stored, so there's no new learning signal.
|
|
79
|
+
if (noveltyThreshold > 0) {
|
|
80
|
+
const novelty = 1 - similarityScore;
|
|
81
|
+
if (novelty < noveltyThreshold) return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Gate 2: Cooldown — prevent same entry from being reinforced too frequently
|
|
85
|
+
if (cooldownMs > 0) {
|
|
86
|
+
const lastReinforced = this.cooldownMap.get(entryId);
|
|
87
|
+
if (lastReinforced && Date.now() - lastReinforced < cooldownMs) return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Record that these memory IDs were returned in a search result.
|
|
95
|
+
* Accumulates deltas in memory; flushed to store after debounce.
|
|
96
|
+
*
|
|
97
|
+
* @param ids Memory entry IDs to record access for
|
|
98
|
+
* @param scores Optional parallel array of similarity scores for novelty gating.
|
|
99
|
+
* When provided, only entries passing shouldReinforce() are recorded.
|
|
100
|
+
*/
|
|
101
|
+
recordAccess(ids: string[], scores?: number[]): void {
|
|
102
|
+
if (ids.length === 0) return;
|
|
103
|
+
|
|
104
|
+
const now = Date.now();
|
|
105
|
+
|
|
106
|
+
for (let i = 0; i < ids.length; i++) {
|
|
107
|
+
const id = ids[i];
|
|
108
|
+
const score = scores?.[i];
|
|
109
|
+
|
|
110
|
+
// If scores provided, apply novelty + cooldown gating
|
|
111
|
+
if (score !== undefined && !this.shouldReinforce(id, score)) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this.pending.set(id, (this.pending.get(id) || 0) + 1);
|
|
116
|
+
this.cooldownMap.set(id, now);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Nothing passed the gate → skip flush scheduling
|
|
120
|
+
if (this.pending.size === 0) return;
|
|
121
|
+
|
|
122
|
+
// Debounce: schedule flush if not already pending
|
|
123
|
+
if (!this.flushTimer) {
|
|
124
|
+
this.flushTimer = setTimeout(() => {
|
|
125
|
+
this.flushTimer = null;
|
|
126
|
+
this.flush().catch(err => {
|
|
127
|
+
logWarn("AccessTracker flush failed:", err);
|
|
128
|
+
});
|
|
129
|
+
}, this.config.flushIntervalMs);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Write accumulated access deltas to store metadata.
|
|
135
|
+
* Also evaluates tier promotion/demotion based on access patterns.
|
|
136
|
+
* Concurrent flush protection: prevents data corruption from overlapping flushes.
|
|
137
|
+
*/
|
|
138
|
+
async flush(): Promise<void> {
|
|
139
|
+
if (this.flushPromise) {
|
|
140
|
+
await this.flushPromise;
|
|
141
|
+
if (this.pending.size > 0) return this.flush();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (this.pending.size === 0) return;
|
|
145
|
+
|
|
146
|
+
this.flushPromise = this.doFlush();
|
|
147
|
+
try {
|
|
148
|
+
await this.flushPromise;
|
|
149
|
+
} finally {
|
|
150
|
+
this.flushPromise = null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private async doFlush(): Promise<void> {
|
|
155
|
+
// Snapshot and clear pending
|
|
156
|
+
const batch = new Map(this.pending);
|
|
157
|
+
this.pending.clear();
|
|
158
|
+
|
|
159
|
+
const now = Date.now();
|
|
160
|
+
let promotions = 0;
|
|
161
|
+
let demotions = 0;
|
|
162
|
+
|
|
163
|
+
for (const [id, delta] of batch) {
|
|
164
|
+
try {
|
|
165
|
+
const entry = await this.store.getById(id);
|
|
166
|
+
if (!entry) continue;
|
|
167
|
+
|
|
168
|
+
const meta = safeParseMetadata(entry.metadata);
|
|
169
|
+
const prevCount = typeof meta.accessCount === "number" ? meta.accessCount : 0;
|
|
170
|
+
const newCount = prevCount + delta;
|
|
171
|
+
const importance = entry.importance ?? 0.6;
|
|
172
|
+
const lastAccessedAt = now;
|
|
173
|
+
|
|
174
|
+
// Evaluate tier change
|
|
175
|
+
const currentTier = resolveTier(entry.metadata);
|
|
176
|
+
const newTier = evaluateTierChange(
|
|
177
|
+
currentTier,
|
|
178
|
+
newCount,
|
|
179
|
+
importance,
|
|
180
|
+
lastAccessedAt,
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const tierChanged = newTier !== currentTier;
|
|
184
|
+
if (tierChanged) {
|
|
185
|
+
if (TIER_ORDER[newTier] > TIER_ORDER[currentTier]) {
|
|
186
|
+
promotions++;
|
|
187
|
+
logInfo(`[INFO] Tier promotion: ${id.slice(0, 8)} ${currentTier} → ${newTier} (access=${newCount})`);
|
|
188
|
+
} else {
|
|
189
|
+
demotions++;
|
|
190
|
+
logInfo(`[INFO] Tier demotion: ${id.slice(0, 8)} ${currentTier} → ${newTier}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const updatedMeta = {
|
|
195
|
+
...meta,
|
|
196
|
+
accessCount: newCount,
|
|
197
|
+
lastAccessedAt,
|
|
198
|
+
...(tierChanged ? { tier: newTier } : {}),
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
await this.store.update(id, { metadata: JSON.stringify(updatedMeta) });
|
|
202
|
+
} catch {
|
|
203
|
+
// Re-queue failed entries for next flush
|
|
204
|
+
this.pending.set(id, (this.pending.get(id) || 0) + delta);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (promotions + demotions > 0) {
|
|
209
|
+
logInfo(`[INFO] Tier changes: ${promotions} promotions, ${demotions} demotions`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Compute effective half-life for a memory entry based on access history.
|
|
215
|
+
*
|
|
216
|
+
* Formula:
|
|
217
|
+
* freshness = exp(-accessAgeDays / accessFreshnessHalfLife)
|
|
218
|
+
* effectiveCount = accessCount * freshness
|
|
219
|
+
* extension = baseHalfLife * reinforcementFactor * log1p(effectiveCount)
|
|
220
|
+
* effectiveHalfLife = min(baseHalfLife + extension, baseHalfLife * maxMultiplier)
|
|
221
|
+
*/
|
|
222
|
+
computeEffectiveHalfLife(baseHalfLife: number, metadata?: string): number {
|
|
223
|
+
if (baseHalfLife <= 0) return baseHalfLife;
|
|
224
|
+
|
|
225
|
+
const meta = safeParseMetadata(metadata);
|
|
226
|
+
const accessCount = typeof meta.accessCount === "number" ? meta.accessCount : 0;
|
|
227
|
+
const lastAccessedAt = typeof meta.lastAccessedAt === "number" ? meta.lastAccessedAt : 0;
|
|
228
|
+
|
|
229
|
+
if (accessCount <= 0 || lastAccessedAt <= 0) return baseHalfLife;
|
|
230
|
+
|
|
231
|
+
const accessAgeDays = (Date.now() - lastAccessedAt) / 86_400_000;
|
|
232
|
+
const freshness = Math.exp(-accessAgeDays / this.config.accessFreshnessHalfLifeDays);
|
|
233
|
+
const effectiveCount = accessCount * freshness;
|
|
234
|
+
|
|
235
|
+
const extension = baseHalfLife * this.config.reinforcementFactor * Math.log1p(effectiveCount);
|
|
236
|
+
const maxHalfLife = baseHalfLife * this.config.maxMultiplier;
|
|
237
|
+
|
|
238
|
+
return Math.min(baseHalfLife + extension, maxHalfLife);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Clean up timers and cooldown state.
|
|
243
|
+
*/
|
|
244
|
+
destroy(): void {
|
|
245
|
+
if (this.flushTimer) {
|
|
246
|
+
clearTimeout(this.flushTimer);
|
|
247
|
+
this.flushTimer = null;
|
|
248
|
+
}
|
|
249
|
+
this.cooldownMap.clear();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
get pendingCount(): number {
|
|
253
|
+
return this.pending.size;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ============================================================================
|
|
258
|
+
// Hotness Score
|
|
259
|
+
// ============================================================================
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Compute a hotness score (0-1) for a memory entry.
|
|
263
|
+
*
|
|
264
|
+
* Combines access frequency (sigmoid of log1p) with recency of last access
|
|
265
|
+
* (exponential decay). Inspired by OpenViking memory_lifecycle.py.
|
|
266
|
+
*
|
|
267
|
+
* @param accessCount Number of times this memory was recalled
|
|
268
|
+
* @param lastAccessMs Timestamp (ms) of last access
|
|
269
|
+
* @param decayRate Decay rate (default 0.1 ≈ 7-day half-life)
|
|
270
|
+
* @returns 0-1 score. High = frequently + recently accessed.
|
|
271
|
+
*/
|
|
272
|
+
export function computeHotnessScore(
|
|
273
|
+
accessCount: number,
|
|
274
|
+
lastAccessMs: number,
|
|
275
|
+
decayRate = 0.1,
|
|
276
|
+
): number {
|
|
277
|
+
if (accessCount <= 0) return 0;
|
|
278
|
+
|
|
279
|
+
// Frequency component: sigmoid of log1p(count) → 0.5..1.0
|
|
280
|
+
const freq = 1 / (1 + Math.exp(-Math.log1p(accessCount)));
|
|
281
|
+
|
|
282
|
+
// Recency component: exponential decay from last access
|
|
283
|
+
const ageDays = Math.max(0, (Date.now() - lastAccessMs) / 86_400_000);
|
|
284
|
+
const recency = Math.exp(-decayRate * ageDays);
|
|
285
|
+
|
|
286
|
+
return freq * recency;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Parse access metadata from a raw metadata string.
|
|
291
|
+
*/
|
|
292
|
+
export function parseAccessMetadata(raw?: string): { accessCount: number; lastAccessedAt: number } {
|
|
293
|
+
const meta = safeParseMetadata(raw);
|
|
294
|
+
return {
|
|
295
|
+
accessCount: typeof meta.accessCount === "number" ? meta.accessCount : 0,
|
|
296
|
+
lastAccessedAt: typeof meta.lastAccessedAt === "number" ? meta.lastAccessedAt : 0,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ============================================================================
|
|
301
|
+
// Helpers
|
|
302
|
+
// ============================================================================
|
|
303
|
+
|
|
304
|
+
const TIER_ORDER: Record<MemoryTier, number> = {
|
|
305
|
+
peripheral: 0,
|
|
306
|
+
working: 1,
|
|
307
|
+
core: 2,
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
function safeParseMetadata(raw?: string): Record<string, unknown> {
|
|
311
|
+
try {
|
|
312
|
+
return JSON.parse(raw || "{}") as Record<string, unknown>;
|
|
313
|
+
} catch {
|
|
314
|
+
return {};
|
|
315
|
+
}
|
|
316
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adaptive Retrieval — skip trivial queries to save embedding API calls.
|
|
3
|
+
* Synced from memory-lancedb-pro upstream with CJK-aware length + force-retrieve.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Force-retrieve: queries with memory-related intent should ALWAYS trigger retrieval
|
|
7
|
+
// even if they are short (e.g. "你记得吗" = 4 chars, would otherwise be skipped)
|
|
8
|
+
const FORCE_RETRIEVE_PATTERNS = [
|
|
9
|
+
/\b(remember|recall|forgot|memory|memories)\b/i,
|
|
10
|
+
/\b(last time|before|previously|earlier|yesterday|ago)\b/i,
|
|
11
|
+
/\b(my (name|email|phone|address|birthday|preference))\b/i,
|
|
12
|
+
/\b(what did (i|we)|did i (tell|say|mention))\b/i,
|
|
13
|
+
/(你记得|记不记得|之前|上次|以前|还记得|提到过|说过)/,
|
|
14
|
+
/(我叫|我的名字|我是谁)/,
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const SKIP_PATTERNS = [
|
|
18
|
+
// Greetings / acks
|
|
19
|
+
/^(hi|hello|hey|good\s*(morning|afternoon|evening|night)|greetings|yo|sup|howdy)\b/i,
|
|
20
|
+
// System / bot commands
|
|
21
|
+
/^\//,
|
|
22
|
+
/^(run|build|test|ls|cd|git|npm|pip|docker|curl|cat|grep|find|make|sudo)\b/i,
|
|
23
|
+
// Short acks (end-anchored to avoid matching real content)
|
|
24
|
+
/^(yes|no|yep|nope|ok|okay|sure|fine|thanks|thank you|thx|ty|got it|understood|cool|nice|great|good|perfect|awesome)\s*[.!]?$/i,
|
|
25
|
+
// Continuations
|
|
26
|
+
/^(go ahead|continue|proceed|do it|start|begin|next|开始|继续|好的|可以|行)\s*[.!]?$/i,
|
|
27
|
+
// Session boilerplate
|
|
28
|
+
/^(fresh session|new session|\/new|\/compact|\/restart)/i,
|
|
29
|
+
/HEARTBEAT/i,
|
|
30
|
+
/^\[System/i,
|
|
31
|
+
// Single emoji or punctuation
|
|
32
|
+
/^[\p{Emoji}\s.,!?。!?,、]+$/u,
|
|
33
|
+
// Single-word pings
|
|
34
|
+
/^(ping|pong|test|debug)\s*[.!?]?$/i,
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Returns true if the query is too trivial to warrant an embedding API call.
|
|
39
|
+
* CJK-aware: uses lower length threshold for Chinese/Japanese/Korean text.
|
|
40
|
+
*/
|
|
41
|
+
export function shouldSkipRetrieval(query: string, minLength?: number): boolean {
|
|
42
|
+
const trimmed = query.trim();
|
|
43
|
+
|
|
44
|
+
// Force retrieve if query has memory-related intent (BEFORE length check)
|
|
45
|
+
if (FORCE_RETRIEVE_PATTERNS.some(p => p.test(trimmed))) return false;
|
|
46
|
+
|
|
47
|
+
// Very short = skip
|
|
48
|
+
if (trimmed.length < 4) return true;
|
|
49
|
+
|
|
50
|
+
// Pattern-based skip
|
|
51
|
+
if (SKIP_PATTERNS.some(p => p.test(trimmed))) return true;
|
|
52
|
+
|
|
53
|
+
// CJK-aware minimum length: Chinese chars carry more semantic density
|
|
54
|
+
const hasCJK = /[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff]/.test(trimmed);
|
|
55
|
+
const effectiveMinLength = minLength ?? (hasCJK ? 6 : 15);
|
|
56
|
+
|
|
57
|
+
// Short non-question messages are skipped; questions (? ?) are always worth checking
|
|
58
|
+
if (trimmed.length < effectiveMinLength &&
|
|
59
|
+
!trimmed.includes('?') && !trimmed.includes('?')) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return false;
|
|
64
|
+
}
|