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/decay-engine.ts
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* No LLM required — pure math + access statistics.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import type { EmotionMetadata } from "./memory-schema.js";
|
|
14
|
+
|
|
13
15
|
// ============================================================================
|
|
14
16
|
// Tier Definitions
|
|
15
17
|
// ============================================================================
|
|
@@ -86,6 +88,88 @@ export function weibullDecay(
|
|
|
86
88
|
return floor + (1 - floor) * decay;
|
|
87
89
|
}
|
|
88
90
|
|
|
91
|
+
// ============================================================================
|
|
92
|
+
// Emotion-Adjusted Decay
|
|
93
|
+
// ============================================================================
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Adjust half-life based on emotional intensity.
|
|
97
|
+
* Strong emotion extends half-life by up to 30%.
|
|
98
|
+
*/
|
|
99
|
+
export function adjustHalfLifeForEmotion(
|
|
100
|
+
baseHalfLife: number,
|
|
101
|
+
emotion: EmotionMetadata | null | undefined,
|
|
102
|
+
): number {
|
|
103
|
+
if (!emotion) return baseHalfLife;
|
|
104
|
+
const raw = Math.abs(emotion.valence);
|
|
105
|
+
// Dead-zone: ignore negligible emotional signal below threshold
|
|
106
|
+
const intensity = raw < 0.1 ? 0 : raw;
|
|
107
|
+
return baseHalfLife * (1 + 0.3 * intensity);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Compute initial strength boost from arousal (flashbulb memory effect).
|
|
112
|
+
* Returns multiplier in [1.0, 1.1].
|
|
113
|
+
*/
|
|
114
|
+
export function computeArousalBoost(
|
|
115
|
+
emotion: EmotionMetadata | null | undefined,
|
|
116
|
+
): number {
|
|
117
|
+
if (!emotion) return 1.0;
|
|
118
|
+
return 1 + 0.1 * emotion.arousal;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ============================================================================
|
|
122
|
+
// HP-7: Decay Exemptions
|
|
123
|
+
// ============================================================================
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Check if a memory should be exempt from time decay.
|
|
127
|
+
* Exempt entries keep their original score without time-based penalties.
|
|
128
|
+
*
|
|
129
|
+
* Exemption rules:
|
|
130
|
+
* 1. Core tier + high importance (≥ 0.95) → identity/correction, must not fade
|
|
131
|
+
* 2. Recently accessed (within 7 days) → actively used, not stale
|
|
132
|
+
* 3. Pinned → user explicitly marked as persistent
|
|
133
|
+
*/
|
|
134
|
+
export function isDecayExempt(
|
|
135
|
+
metadata: string | undefined,
|
|
136
|
+
importance: number,
|
|
137
|
+
): boolean {
|
|
138
|
+
if (!metadata) return false;
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const meta = JSON.parse(metadata) as Record<string, unknown>;
|
|
142
|
+
|
|
143
|
+
// Rule 1: Core + very high importance
|
|
144
|
+
const tier = meta.tier ?? resolveTierFromMeta(meta);
|
|
145
|
+
if (tier === "core" && importance >= 0.95) return true;
|
|
146
|
+
|
|
147
|
+
// Rule 2: Accessed within last 7 days
|
|
148
|
+
const lastAccess = typeof meta.lastAccessedAt === "number" ? meta.lastAccessedAt : 0;
|
|
149
|
+
if (lastAccess > 0 && (Date.now() - lastAccess) < 7 * 86_400_000) return true;
|
|
150
|
+
|
|
151
|
+
// Rule 3: Pinned
|
|
152
|
+
const tags = Array.isArray(meta.tags) ? meta.tags : [];
|
|
153
|
+
if (tags.includes("pinned")) return true;
|
|
154
|
+
|
|
155
|
+
return false;
|
|
156
|
+
} catch {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Internal helper: resolve tier from parsed metadata (avoids re-parsing). */
|
|
162
|
+
function resolveTierFromMeta(meta: Record<string, unknown>): MemoryTier {
|
|
163
|
+
if (meta.tier === "core" || meta.tier === "working" || meta.tier === "peripheral") {
|
|
164
|
+
return meta.tier;
|
|
165
|
+
}
|
|
166
|
+
const imp = typeof meta.importance === "number" ? meta.importance : 0;
|
|
167
|
+
const ac = typeof meta.accessCount === "number" ? meta.accessCount : 0;
|
|
168
|
+
if (imp >= 0.95 || ac >= 10) return "core";
|
|
169
|
+
if (imp >= 0.8 || ac >= 3) return "working";
|
|
170
|
+
return "peripheral";
|
|
171
|
+
}
|
|
172
|
+
|
|
89
173
|
// ============================================================================
|
|
90
174
|
// Tier Resolution
|
|
91
175
|
// ============================================================================
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CC-6: Distill gate — PID-based lock file + session gate.
|
|
3
|
+
* Lock file mtime serves as lastDistillAt timestamp (no separate field).
|
|
4
|
+
*
|
|
5
|
+
* - acquireLock(): write current PID, check existing lock PID liveness + mtime expiry
|
|
6
|
+
* - releaseLock(): delete lock file (mtime naturally updates to now on success)
|
|
7
|
+
* - rollbackLock(previousMtime): on failure, rewind lock mtime so next run retries
|
|
8
|
+
* - shouldDistill(checkpointCount): session gate — checkpoint count < 3 → skip
|
|
9
|
+
* - getLastDistillTime(): read lock file mtime, return 0 if absent
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
mkdirSync,
|
|
14
|
+
writeFileSync,
|
|
15
|
+
readFileSync,
|
|
16
|
+
unlinkSync,
|
|
17
|
+
statSync,
|
|
18
|
+
utimesSync,
|
|
19
|
+
existsSync,
|
|
20
|
+
} from "node:fs";
|
|
21
|
+
import { join, dirname } from "node:path";
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Config
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
export interface DistillLockConfig {
|
|
28
|
+
/** Path to the lock file (default: data/distill.lock) */
|
|
29
|
+
lockPath: string;
|
|
30
|
+
/** Lock expiry in ms (default: 3600000 = 1h) */
|
|
31
|
+
expireMs: number;
|
|
32
|
+
/** Minimum checkpoint count before distill is allowed (default: 3) */
|
|
33
|
+
minCheckpoints: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const DEFAULT_DISTILL_LOCK_CONFIG: DistillLockConfig = {
|
|
37
|
+
lockPath: join(process.env.RECALLNEST_DATA_DIR || "data", "distill.lock"),
|
|
38
|
+
expireMs: 3_600_000,
|
|
39
|
+
minCheckpoints: 3,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Helpers
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
function resolveConfig(cfg?: Partial<DistillLockConfig>): DistillLockConfig {
|
|
47
|
+
return { ...DEFAULT_DISTILL_LOCK_CONFIG, ...cfg };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isPidAlive(pid: number): boolean {
|
|
51
|
+
try {
|
|
52
|
+
process.kill(pid, 0);
|
|
53
|
+
return true;
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Public API
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Try to acquire the distill lock.
|
|
65
|
+
* Returns true if lock was acquired, false if another live process holds it.
|
|
66
|
+
*/
|
|
67
|
+
export function acquireLock(cfg?: Partial<DistillLockConfig>): boolean {
|
|
68
|
+
const { lockPath, expireMs } = resolveConfig(cfg);
|
|
69
|
+
|
|
70
|
+
if (existsSync(lockPath)) {
|
|
71
|
+
const existingPid = parseInt(readFileSync(lockPath, "utf-8").trim(), 10);
|
|
72
|
+
const mtime = statSync(lockPath).mtimeMs;
|
|
73
|
+
const expired = Date.now() - mtime > expireMs;
|
|
74
|
+
|
|
75
|
+
if (!isNaN(existingPid) && isPidAlive(existingPid) && !expired) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
// PID dead or lock expired — overwrite
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
mkdirSync(dirname(lockPath), { recursive: true });
|
|
82
|
+
writeFileSync(lockPath, String(process.pid), "utf-8");
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Release the distill lock by removing the lock file. */
|
|
87
|
+
export function releaseLock(cfg?: Partial<DistillLockConfig>): void {
|
|
88
|
+
const { lockPath } = resolveConfig(cfg);
|
|
89
|
+
if (existsSync(lockPath)) {
|
|
90
|
+
unlinkSync(lockPath);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Roll back lock mtime to a previous value so the next run retries sooner.
|
|
96
|
+
* Used when distill fails and we want to preserve the "last success" timestamp.
|
|
97
|
+
*/
|
|
98
|
+
export function rollbackLock(
|
|
99
|
+
previousMtime: Date,
|
|
100
|
+
cfg?: Partial<DistillLockConfig>,
|
|
101
|
+
): void {
|
|
102
|
+
const { lockPath } = resolveConfig(cfg);
|
|
103
|
+
if (existsSync(lockPath)) {
|
|
104
|
+
utimesSync(lockPath, previousMtime, previousMtime);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Session gate: returns true when enough checkpoints have accumulated. */
|
|
109
|
+
export function shouldDistill(
|
|
110
|
+
checkpointCountSinceLastDistill: number,
|
|
111
|
+
cfg?: Partial<DistillLockConfig>,
|
|
112
|
+
): boolean {
|
|
113
|
+
const { minCheckpoints } = resolveConfig(cfg);
|
|
114
|
+
return checkpointCountSinceLastDistill >= minCheckpoints;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Read lock file mtime as the last-distill timestamp. Returns 0 if absent. */
|
|
118
|
+
export function getLastDistillTime(cfg?: Partial<DistillLockConfig>): number {
|
|
119
|
+
const { lockPath } = resolveConfig(cfg);
|
|
120
|
+
if (!existsSync(lockPath)) return 0;
|
|
121
|
+
return statSync(lockPath).mtimeMs;
|
|
122
|
+
}
|
package/src/doctor.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { existsSync, accessSync, constants, readFileSync } from "node:fs";
|
|
14
14
|
import { resolve, join } from "node:path";
|
|
15
15
|
import { homedir } from "node:os";
|
|
16
|
+
import { metaDir } from "./compat.js";
|
|
16
17
|
import { loadConfig, expandHome, resolveEnv, findConfigPath, loadDotEnv } from "./runtime-config.js";
|
|
17
18
|
import { createEmbedder, type EmbeddingConfig } from "./embedder.js";
|
|
18
19
|
import { extractCanonicalKey, normalizeCanonicalKey } from "./memory-boundaries.js";
|
|
@@ -96,15 +97,15 @@ export function assessScopeInventoryReport(report: ScopeInventoryReport): CheckR
|
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
function workflowPatternSeedsPath(): string {
|
|
99
|
-
return resolve(import.meta
|
|
100
|
+
return resolve(metaDir(import.meta), "../eval/continuity/pattern-seeds.json");
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
function caseMemorySeedsPath(): string {
|
|
103
|
-
return resolve(import.meta
|
|
104
|
+
return resolve(metaDir(import.meta), "../eval/continuity/case-seeds.json");
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
function memorySeedsPath(): string {
|
|
107
|
-
return resolve(import.meta
|
|
108
|
+
return resolve(metaDir(import.meta), "../eval/continuity/memory-seeds.json");
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
function parseMetadata(raw?: string): Record<string, unknown> {
|
|
@@ -295,16 +296,13 @@ export function assessContinuityBaseline(
|
|
|
295
296
|
export async function runDoctor(options: { ci?: boolean } = {}): Promise<CheckResult[]> {
|
|
296
297
|
const results: CheckResult[] = [];
|
|
297
298
|
|
|
298
|
-
// 1.
|
|
299
|
+
// 1. Runtime check
|
|
299
300
|
const bunVersion = typeof Bun !== "undefined" ? Bun.version : null;
|
|
300
301
|
if (bunVersion) {
|
|
301
|
-
results.push(pass("
|
|
302
|
+
results.push(pass("Runtime", `Bun v${bunVersion}`));
|
|
302
303
|
} else {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
"Bun not detected (running under Node?)",
|
|
306
|
-
"Install Bun: curl -fsSL https://bun.sh/install | bash"
|
|
307
|
-
));
|
|
304
|
+
const nodeVersion = process.version;
|
|
305
|
+
results.push(pass("Runtime", `Node.js ${nodeVersion} (Bun recommended for best performance)`));
|
|
308
306
|
}
|
|
309
307
|
|
|
310
308
|
// 2. Config file
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AD-1: Unified Dream Pipeline
|
|
3
|
+
*
|
|
4
|
+
* Inspired by Claude Code's Auto Dream — a four-phase consolidation pipeline
|
|
5
|
+
* that orchestrates existing RecallNest components into a coherent "dream" cycle.
|
|
6
|
+
*
|
|
7
|
+
* Phases:
|
|
8
|
+
* 1. Orient — scan memory state, get latest checkpoint, assess activity
|
|
9
|
+
* 2. Gather — collect recent signals (new writes since last dream)
|
|
10
|
+
* 3. Consolidate — cluster, merge, extract patterns, generate insights
|
|
11
|
+
* 4. Prune — archive low-value memories, enforce storage hygiene
|
|
12
|
+
*
|
|
13
|
+
* Unlike Auto Dream's grep-only approach, RecallNest uses vector search +
|
|
14
|
+
* LLM-driven consolidation for semantic-level maintenance.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { MemoryStore } from "./store.js";
|
|
18
|
+
import type { LLMClient } from "./llm-client.js";
|
|
19
|
+
import type { Embedder } from "./embedder.js";
|
|
20
|
+
import { ConsolidationEngine, clusterAndConsolidate, DEFAULT_CONSOLIDATION_CONFIG } from "./consolidation-engine.js";
|
|
21
|
+
import { maybeRunGc, type GcResult, type AutoGcConfig, DEFAULT_AUTO_GC_CONFIG } from "./auto-gc.js";
|
|
22
|
+
import { getWriteCount, resetWriteCount } from "./activity-counter.js";
|
|
23
|
+
import { isActiveMemory } from "./memory-evolution.js";
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Types
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
export interface DreamConfig {
|
|
30
|
+
/** Minimum writes since last dream to justify running (default: 10) */
|
|
31
|
+
minWritesForDream: number;
|
|
32
|
+
/** Consolidation cluster threshold (default: 0.82) */
|
|
33
|
+
clusterThreshold: number;
|
|
34
|
+
/** Minimum cluster size for insight/pattern generation (default: 3) */
|
|
35
|
+
minClusterSize: number;
|
|
36
|
+
/** Enable cross-memory pattern extraction in consolidation (default: true) */
|
|
37
|
+
extractPatterns: boolean;
|
|
38
|
+
/** Max entries to scan per consolidation run (default: 500) */
|
|
39
|
+
maxEntriesPerRun: number;
|
|
40
|
+
/** GC config for prune phase */
|
|
41
|
+
gc: AutoGcConfig;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const DEFAULT_DREAM_CONFIG: DreamConfig = {
|
|
45
|
+
minWritesForDream: 10,
|
|
46
|
+
clusterThreshold: 0.82,
|
|
47
|
+
minClusterSize: 3,
|
|
48
|
+
extractPatterns: true,
|
|
49
|
+
maxEntriesPerRun: 500,
|
|
50
|
+
gc: DEFAULT_AUTO_GC_CONFIG,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export interface DreamPhaseResult {
|
|
54
|
+
phase: "orient" | "gather" | "consolidate" | "prune";
|
|
55
|
+
detail: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface DreamResult {
|
|
59
|
+
ran: boolean;
|
|
60
|
+
reason?: string;
|
|
61
|
+
phases: DreamPhaseResult[];
|
|
62
|
+
stats: {
|
|
63
|
+
totalMemories: number;
|
|
64
|
+
activeMemories: number;
|
|
65
|
+
writesSinceLastDream: number;
|
|
66
|
+
clustersFound: number;
|
|
67
|
+
insightsGenerated: number;
|
|
68
|
+
patternsExtracted: number;
|
|
69
|
+
mergedCount: number;
|
|
70
|
+
archivedCount: number;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Pipeline
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
export async function runDream(params: {
|
|
79
|
+
store: MemoryStore;
|
|
80
|
+
llm: LLMClient | null;
|
|
81
|
+
embedder: Pick<Embedder, "embedPassage">;
|
|
82
|
+
scope: string;
|
|
83
|
+
config?: Partial<DreamConfig>;
|
|
84
|
+
/** Skip the minimum-writes gate (force run) */
|
|
85
|
+
force?: boolean;
|
|
86
|
+
}): Promise<DreamResult> {
|
|
87
|
+
const { store, llm, embedder, scope, force = false } = params;
|
|
88
|
+
const config = { ...DEFAULT_DREAM_CONFIG, ...params.config };
|
|
89
|
+
const phases: DreamPhaseResult[] = [];
|
|
90
|
+
|
|
91
|
+
const stats = {
|
|
92
|
+
totalMemories: 0,
|
|
93
|
+
activeMemories: 0,
|
|
94
|
+
writesSinceLastDream: 0,
|
|
95
|
+
clustersFound: 0,
|
|
96
|
+
insightsGenerated: 0,
|
|
97
|
+
patternsExtracted: 0,
|
|
98
|
+
mergedCount: 0,
|
|
99
|
+
archivedCount: 0,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// =========================================================================
|
|
103
|
+
// Phase 1: Orient — assess current memory state
|
|
104
|
+
// =========================================================================
|
|
105
|
+
const writeCount = getWriteCount();
|
|
106
|
+
stats.writesSinceLastDream = writeCount;
|
|
107
|
+
|
|
108
|
+
const storeStats = await store.stats([scope]);
|
|
109
|
+
stats.totalMemories = storeStats.totalCount ?? 0;
|
|
110
|
+
|
|
111
|
+
if (!force && writeCount < config.minWritesForDream) {
|
|
112
|
+
return {
|
|
113
|
+
ran: false,
|
|
114
|
+
reason: `insufficient_writes (${writeCount}/${config.minWritesForDream})`,
|
|
115
|
+
phases: [{
|
|
116
|
+
phase: "orient",
|
|
117
|
+
detail: `${stats.totalMemories} memories, ${writeCount} writes since last dream — below threshold`,
|
|
118
|
+
}],
|
|
119
|
+
stats,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
phases.push({
|
|
124
|
+
phase: "orient",
|
|
125
|
+
detail: `${stats.totalMemories} memories, ${writeCount} writes since last dream`,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// =========================================================================
|
|
129
|
+
// Phase 2: Gather — collect active entries for consolidation
|
|
130
|
+
// =========================================================================
|
|
131
|
+
const entries = await store.list([scope], undefined, config.maxEntriesPerRun, 0);
|
|
132
|
+
const active = entries.filter(e => isActiveMemory(e.metadata));
|
|
133
|
+
stats.activeMemories = active.length;
|
|
134
|
+
|
|
135
|
+
phases.push({
|
|
136
|
+
phase: "gather",
|
|
137
|
+
detail: `${active.length} active entries gathered from ${entries.length} total`,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (active.length < config.minClusterSize) {
|
|
141
|
+
phases.push({ phase: "consolidate", detail: "skipped — too few active entries" });
|
|
142
|
+
phases.push({ phase: "prune", detail: "skipped — too few entries for GC" });
|
|
143
|
+
resetWriteCount();
|
|
144
|
+
return { ran: true, reason: "completed_early", phases, stats };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// =========================================================================
|
|
148
|
+
// Phase 3: Consolidate — cluster, merge, generate insights + patterns
|
|
149
|
+
// =========================================================================
|
|
150
|
+
|
|
151
|
+
// 3a: Deterministic consolidation (merge near-duplicates, link clusters)
|
|
152
|
+
const engine = new ConsolidationEngine(store, {
|
|
153
|
+
...DEFAULT_CONSOLIDATION_CONFIG,
|
|
154
|
+
clusterThreshold: config.clusterThreshold,
|
|
155
|
+
maxEntriesPerRun: config.maxEntriesPerRun,
|
|
156
|
+
});
|
|
157
|
+
const consolidation = await engine.run(scope);
|
|
158
|
+
stats.clustersFound += consolidation.clustersFound;
|
|
159
|
+
stats.mergedCount += consolidation.mergedCount;
|
|
160
|
+
|
|
161
|
+
// 3b: LLM-driven cluster consolidation (insights + patterns) — requires LLM
|
|
162
|
+
if (llm) {
|
|
163
|
+
const clusterResult = await clusterAndConsolidate({
|
|
164
|
+
entries: active,
|
|
165
|
+
embedder,
|
|
166
|
+
llm,
|
|
167
|
+
store,
|
|
168
|
+
scope,
|
|
169
|
+
minClusterSize: config.minClusterSize,
|
|
170
|
+
clusterThreshold: config.clusterThreshold - 0.07, // slightly lower for semantic clustering
|
|
171
|
+
extractPatterns: config.extractPatterns,
|
|
172
|
+
});
|
|
173
|
+
stats.clustersFound += clusterResult.clustersFound;
|
|
174
|
+
stats.insightsGenerated = clusterResult.insightsGenerated;
|
|
175
|
+
stats.patternsExtracted = clusterResult.patternsExtracted;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
phases.push({
|
|
179
|
+
phase: "consolidate",
|
|
180
|
+
detail: `${stats.clustersFound} clusters, ${stats.mergedCount} merged, ${stats.insightsGenerated} insights, ${stats.patternsExtracted} patterns`,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// =========================================================================
|
|
184
|
+
// Phase 4: Prune — archive low-value memories
|
|
185
|
+
// =========================================================================
|
|
186
|
+
const gcResult = await maybeRunGc(store, config.gc);
|
|
187
|
+
stats.archivedCount = gcResult.archivedCount;
|
|
188
|
+
|
|
189
|
+
phases.push({
|
|
190
|
+
phase: "prune",
|
|
191
|
+
detail: gcResult.triggered
|
|
192
|
+
? `${gcResult.archivedCount} entries archived`
|
|
193
|
+
: `skipped — ${gcResult.reason}`,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Reset activity counter after successful dream
|
|
197
|
+
resetWriteCount();
|
|
198
|
+
|
|
199
|
+
return { ran: true, phases, stats };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// Formatting
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
export function formatDreamResult(result: DreamResult): string {
|
|
207
|
+
if (!result.ran) {
|
|
208
|
+
return `Dream skipped: ${result.reason}\n${result.phases[0]?.detail ?? ""}`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const lines = [
|
|
212
|
+
"Dream completed",
|
|
213
|
+
"",
|
|
214
|
+
...result.phases.map(p => `[${p.phase}] ${p.detail}`),
|
|
215
|
+
"",
|
|
216
|
+
"Stats:",
|
|
217
|
+
` Total memories: ${result.stats.totalMemories}`,
|
|
218
|
+
` Active: ${result.stats.activeMemories}`,
|
|
219
|
+
` Clusters: ${result.stats.clustersFound}`,
|
|
220
|
+
` Insights: ${result.stats.insightsGenerated}`,
|
|
221
|
+
` Patterns: ${result.stats.patternsExtracted}`,
|
|
222
|
+
` Merged: ${result.stats.mergedCount}`,
|
|
223
|
+
` Archived: ${result.stats.archivedCount}`,
|
|
224
|
+
];
|
|
225
|
+
|
|
226
|
+
return lines.join("\n");
|
|
227
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { EmotionMetadata } from "./memory-schema.js";
|
|
2
|
+
import { isEmotionScoringEnabled } from "./memory-schema.js";
|
|
3
|
+
|
|
4
|
+
const NEGATIVE_SIGNALS: string[] = [
|
|
5
|
+
// English
|
|
6
|
+
"fail", "failed", "failure", "broken", "bug", "error", "wrong", "crash",
|
|
7
|
+
"frustrat", "hate", "terrible", "awful", "annoying", "pain", "stuck",
|
|
8
|
+
"problem", "issue", "mess", "ugly", "worst",
|
|
9
|
+
// Chinese
|
|
10
|
+
"失败", "痛苦", "困扰", "崩溃", "报错", "出错", "难受", "烦",
|
|
11
|
+
"卡住", "折腾", "头疼", "坑", "讨厌", "不喜欢", "糟糕", "恶心",
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const POSITIVE_SIGNALS: string[] = [
|
|
15
|
+
// English
|
|
16
|
+
"solved", "fixed", "works", "perfect", "great", "love", "excellent",
|
|
17
|
+
"success", "breakthrough", "finally", "awesome", "beautiful", "clean",
|
|
18
|
+
"elegant", "smooth", "done", "shipped",
|
|
19
|
+
// Chinese
|
|
20
|
+
"搞定", "成功", "突破", "完美", "太好了", "顺利", "解决", "漂亮",
|
|
21
|
+
"优雅", "通过", "上线", "喜欢", "开心", "厉害",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const HIGH_AROUSAL_SIGNALS: string[] = [
|
|
25
|
+
"!", "!!", "urgent", "critical", "immediately", "ASAP", "emergency",
|
|
26
|
+
"紧急", "立刻", "马上", "赶紧", "救命",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/** Chinese negation prefixes that flip positive → negative */
|
|
30
|
+
const CN_NEGATION_PREFIXES = ["不", "没", "非", "无", "别", "未"];
|
|
31
|
+
|
|
32
|
+
function clamp(value: number, min: number, max: number): number {
|
|
33
|
+
return Math.max(min, Math.min(max, value));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Count positive signal matches, handling Chinese negation.
|
|
38
|
+
* "不喜欢" → skip positive, add to negatedCount (caller adds to negative).
|
|
39
|
+
* Returns { positive, negated } counts.
|
|
40
|
+
*/
|
|
41
|
+
function countPositiveSignals(lower: string): { positive: number; negated: number } {
|
|
42
|
+
let positive = 0;
|
|
43
|
+
let negated = 0;
|
|
44
|
+
for (const signal of POSITIVE_SIGNALS) {
|
|
45
|
+
const s = signal.toLowerCase();
|
|
46
|
+
const idx = lower.indexOf(s);
|
|
47
|
+
if (idx === -1) continue;
|
|
48
|
+
const isNegated = idx > 0 && CN_NEGATION_PREFIXES.some(neg => lower.substring(idx - neg.length, idx) === neg);
|
|
49
|
+
if (isNegated) {
|
|
50
|
+
negated++;
|
|
51
|
+
} else {
|
|
52
|
+
positive++;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { positive, negated };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Detect emotional valence and arousal from text using keyword heuristics.
|
|
60
|
+
* Zero LLM cost. Returns neutral emotion for empty text.
|
|
61
|
+
*/
|
|
62
|
+
export function detectEmotion(text: string): EmotionMetadata {
|
|
63
|
+
if (!text || text.length === 0) {
|
|
64
|
+
return { valence: 0, arousal: 0, label: "neutral", salience: 0, source: "keyword" };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const lower = text.toLowerCase();
|
|
68
|
+
|
|
69
|
+
const negCount = NEGATIVE_SIGNALS.filter(s => lower.includes(s.toLowerCase())).length;
|
|
70
|
+
const { positive: posCount, negated: negatedPosCount } = countPositiveSignals(lower);
|
|
71
|
+
const arousalCount = HIGH_AROUSAL_SIGNALS.filter(s => text.includes(s)).length;
|
|
72
|
+
|
|
73
|
+
// Negated positive signals (e.g. "不开心") count as additional negatives
|
|
74
|
+
const valence = clamp((posCount - negCount - negatedPosCount) * 0.3, -1, 1);
|
|
75
|
+
const arousal = clamp(arousalCount * 0.25, 0, 1);
|
|
76
|
+
const label = valence > 0.25 ? "positive" : valence < -0.25 ? "negative" : "neutral";
|
|
77
|
+
|
|
78
|
+
// Salience: composite mnemonic significance — average of emotional intensity and arousal
|
|
79
|
+
const salience = clamp((Math.abs(valence) + arousal) / 2, 0, 1);
|
|
80
|
+
|
|
81
|
+
return { valence, arousal, label, salience, source: "keyword" };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Conditionally detect emotion. Returns null when feature flag is off.
|
|
86
|
+
*/
|
|
87
|
+
export function detectEmotionIfEnabled(text: string): EmotionMetadata | null {
|
|
88
|
+
if (!isEmotionScoringEnabled()) return null;
|
|
89
|
+
return detectEmotion(text);
|
|
90
|
+
}
|
package/src/eval.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { dirname, resolve } from "node:path";
|
|
5
5
|
|
|
6
|
+
import { metaDir } from "./compat.js";
|
|
6
7
|
import { buildSessionCheckpointRecord, normalizeCheckpointScope } from "./session-engine.js";
|
|
7
8
|
import type { ResumeContextResponse, SessionCheckpointRecord } from "./session-schema.js";
|
|
8
9
|
import { composeResumeContext } from "./context-composer.js";
|
|
@@ -158,8 +159,8 @@ function parseArgs(args: string[]): EvalArgs {
|
|
|
158
159
|
|
|
159
160
|
function defaultCasesPath(mode: EvalMode): string {
|
|
160
161
|
return mode === "continuity"
|
|
161
|
-
? resolve(import.meta
|
|
162
|
-
: resolve(import.meta
|
|
162
|
+
? resolve(metaDir(import.meta), "../eval/continuity/cases.json")
|
|
163
|
+
: resolve(metaDir(import.meta), "../eval/cases.json");
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
function loadCases<T>(mode: EvalMode, pathArg?: string): T[] {
|