recallnest 2.3.0 → 2.6.0
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/CHANGELOG.md +187 -0
- package/README.md +49 -38
- package/README_CN.md +46 -31
- package/assets/ui/app.js +1031 -0
- package/assets/ui/index.html +224 -0
- package/assets/ui/styles.css +781 -0
- package/config.json.example +4 -1
- package/docs/api-reference.md +1291 -0
- package/docs/architecture.md +168 -0
- package/docs/connector-spec.md +126 -0
- package/eval/cases-canary.json +58 -0
- package/eval/cases-scope-robustness.json +278 -0
- package/eval/cases.json +117 -20
- package/package.json +25 -4
- package/src/__tests__/access-tracker-novelty.test.ts +304 -0
- package/src/__tests__/activity-counter.test.ts +125 -0
- package/src/__tests__/adaptive-fidelity.test.ts +102 -0
- package/src/__tests__/admission-control.test.ts +206 -0
- package/src/__tests__/anchor-generator.test.ts +67 -0
- package/src/__tests__/api-server-security.test.ts +102 -0
- package/src/__tests__/auto-consolidation.test.ts +168 -0
- package/src/__tests__/auto-recall.test.ts +131 -0
- package/src/__tests__/babel-integration.test.ts +144 -0
- package/src/__tests__/batch-dedup.test.ts +65 -0
- package/src/__tests__/belief-history.test.ts +220 -0
- package/src/__tests__/canary-eval.test.ts +114 -0
- package/src/__tests__/capture-engine.test.ts +1328 -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 +116 -0
- package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
- package/src/__tests__/circuit-breaker.test.ts +99 -0
- package/src/__tests__/cluster-consolidation.test.ts +945 -0
- package/src/__tests__/confidence-metadata.test.ts +125 -0
- package/src/__tests__/confidence-tracker.test.ts +261 -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 +418 -0
- package/src/__tests__/connector-v1.test.ts +181 -0
- package/src/__tests__/consolidation-engine.test.ts +430 -0
- package/src/__tests__/constructive-retrieval.test.ts +509 -0
- package/src/__tests__/context-collapse-renderer.test.ts +214 -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-reconstructor.test.ts +65 -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__/contracts.test.ts +262 -0
- package/src/__tests__/conversation-importer.test.ts +692 -0
- package/src/__tests__/core-recall-eval.test.ts +235 -0
- package/src/__tests__/core-summary.test.ts +109 -0
- package/src/__tests__/data-checkup.test.ts +276 -0
- package/src/__tests__/decay-exemption.test.ts +129 -0
- package/src/__tests__/dedup-actions.test.ts +58 -0
- package/src/__tests__/dedup-guard.test.ts +39 -0
- package/src/__tests__/deterministic-id.test.ts +39 -0
- package/src/__tests__/distill-lock.test.ts +305 -0
- package/src/__tests__/distill-session-argshape.test.ts +79 -0
- package/src/__tests__/doctor.test.ts +319 -0
- package/src/__tests__/dream-pipeline.test.ts +780 -0
- package/src/__tests__/drill-down.test.ts +108 -0
- package/src/__tests__/embedder.test.ts +122 -0
- package/src/__tests__/emotion-decay.test.ts +46 -0
- package/src/__tests__/emotion-detector.test.ts +56 -0
- package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
- package/src/__tests__/emotion-retrieval.test.ts +56 -0
- package/src/__tests__/entity-resolver.test.ts +58 -0
- package/src/__tests__/env-config.test.ts +151 -0
- package/src/__tests__/error-signature-boost.test.ts +87 -0
- package/src/__tests__/error-signature.test.ts +95 -0
- package/src/__tests__/error-taxonomy.test.ts +129 -0
- package/src/__tests__/ethics-layer.test.ts +476 -0
- package/src/__tests__/event-segmenter.test.ts +153 -0
- package/src/__tests__/feature-flags-integration.test.ts +54 -0
- package/src/__tests__/frequency-tracker.test.ts +113 -0
- package/src/__tests__/freshness.test.ts +186 -0
- package/src/__tests__/graph-export.test.ts +489 -0
- package/src/__tests__/hotness-blend.test.ts +283 -0
- package/src/__tests__/ingest-chunking.test.ts +44 -0
- package/src/__tests__/ingest.test.ts +360 -0
- package/src/__tests__/interference-detector.test.ts +203 -0
- package/src/__tests__/kg-extractor.test.ts +192 -0
- package/src/__tests__/kg-store.test.ts +300 -0
- package/src/__tests__/llm-client-pivot.test.ts +628 -0
- package/src/__tests__/llm-consolidation.test.ts +226 -0
- package/src/__tests__/maintenance-locks.test.ts +189 -0
- package/src/__tests__/mcp-efficiency.test.ts +288 -0
- package/src/__tests__/mcp-related-scopes.test.ts +151 -0
- package/src/__tests__/memory-boundaries.test.ts +282 -0
- package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +345 -0
- package/src/__tests__/memory-evolution.test.ts +271 -0
- package/src/__tests__/memory-health-rebalance.test.ts +144 -0
- package/src/__tests__/memory-lint.test.ts +421 -0
- package/src/__tests__/memory-output.test.ts +215 -0
- package/src/__tests__/memory-promotion.test.ts +315 -0
- package/src/__tests__/migrate-dedup-ids.test.ts +65 -0
- package/src/__tests__/multi-vector.test.ts +242 -0
- package/src/__tests__/narrative-architecture.test.ts +456 -0
- package/src/__tests__/noise-filter.test.ts +205 -0
- package/src/__tests__/noise-prototype-bank.test.ts +280 -0
- package/src/__tests__/obsidian-connector.test.ts +363 -0
- package/src/__tests__/omitted-reporting.test.ts +98 -0
- package/src/__tests__/pending-review.test.ts +77 -0
- package/src/__tests__/pii-redact.test.ts +333 -0
- package/src/__tests__/pivot-apply.test.ts +373 -0
- package/src/__tests__/pivot-distill-supervisor.test.ts +457 -0
- package/src/__tests__/pivot-distill.test.ts +1178 -0
- package/src/__tests__/post-retrieval-filter.test.ts +178 -0
- package/src/__tests__/ppr-traversal.test.ts +256 -0
- package/src/__tests__/prediction-engine.test.ts +349 -0
- package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
- package/src/__tests__/preference-guard-smoke.test.ts +211 -0
- package/src/__tests__/preference-matcher.test.ts +282 -0
- package/src/__tests__/preference-slots.test.ts +205 -0
- package/src/__tests__/prospective-memory.test.ts +283 -0
- package/src/__tests__/pull-from-macbook-sync-only.test.ts +679 -0
- package/src/__tests__/query-entity-detector.test.ts +171 -0
- package/src/__tests__/query-expander-alias.test.ts +146 -0
- package/src/__tests__/query-expander.test.ts +34 -0
- package/src/__tests__/recall-gate.test.ts +266 -0
- package/src/__tests__/recall-governor.test.ts +227 -0
- package/src/__tests__/recall-mode.test.ts +64 -0
- package/src/__tests__/resolve-db-path.test.ts +130 -0
- package/src/__tests__/resolve-tier.test.ts +64 -0
- package/src/__tests__/result-synthesizer.test.ts +198 -0
- package/src/__tests__/resume-context.test.ts +4898 -0
- package/src/__tests__/retrieval-eval.test.ts +163 -0
- package/src/__tests__/retrieval-trace.test.ts +213 -0
- package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
- package/src/__tests__/retriever-lazy-init-dispatch.test.ts +123 -0
- package/src/__tests__/retroactive-boost.test.ts +223 -0
- package/src/__tests__/salience-scoring.test.ts +106 -0
- package/src/__tests__/scope-inventory.test.ts +200 -0
- package/src/__tests__/scope-policy.test.ts +69 -0
- package/src/__tests__/scope-suggester.test.ts +80 -0
- package/src/__tests__/session-checkpoint.test.ts +291 -0
- package/src/__tests__/session-distiller.test.ts +640 -0
- package/src/__tests__/session-gc.test.ts +215 -0
- package/src/__tests__/skill-engine.test.ts +552 -0
- package/src/__tests__/skill-promotion.test.ts +654 -0
- package/src/__tests__/skill-verifier.test.ts +122 -0
- package/src/__tests__/source-heartbeat.test.ts +250 -0
- package/src/__tests__/stable-context-length-contract.test.ts +62 -0
- package/src/__tests__/store-patch-metadata.test.ts +205 -0
- package/src/__tests__/store-read-consistency.test.ts +122 -0
- package/src/__tests__/store-schema-migration.test.ts +87 -0
- package/src/__tests__/store-scope-audit.test.ts +81 -0
- package/src/__tests__/store-upsert.test.ts +201 -0
- package/src/__tests__/supersede-chain.test.ts +154 -0
- package/src/__tests__/temporal-parser.test.ts +241 -0
- package/src/__tests__/temporal-validity.test.ts +160 -0
- package/src/__tests__/term-registry-terminal-roster.test.ts +127 -0
- package/src/__tests__/term-registry.test.ts +121 -0
- package/src/__tests__/tool-error-surfacing.test.ts +98 -0
- package/src/__tests__/tool-output-compressor.test.ts +160 -0
- package/src/__tests__/topic-tag.test.ts +113 -0
- package/src/__tests__/trusted-memory.test.ts +839 -0
- package/src/__tests__/ui-server-security.test.ts +93 -0
- package/src/__tests__/usage-tracker.test.ts +287 -0
- package/src/__tests__/version-manager.test.ts +213 -0
- package/src/__tests__/workflow-observation-skill-outcome.test.ts +352 -0
- package/src/__tests__/workflow-observation.test.ts +433 -0
- package/src/__tests__/write-verifier.test.ts +122 -0
- package/src/access-tracker.ts +90 -34
- package/src/activity-counter.ts +46 -18
- package/src/adaptive-retrieval.ts +11 -0
- package/src/aliases.ts +133 -0
- package/src/api-server.ts +36 -9
- package/src/audit-log.ts +4 -2
- package/src/auto-consolidation.ts +8 -3
- package/src/auto-gc.ts +85 -21
- package/src/auto-recall.ts +61 -1
- package/src/belief-history.ts +164 -0
- package/src/capture-engine.ts +142 -32
- package/src/cli.ts +268 -41
- package/src/confidence-tracker.ts +36 -18
- package/src/conflict-engine.ts +24 -3
- package/src/consolidation-engine.ts +255 -23
- package/src/context-composer-stable-selection.ts +7 -1
- package/src/context-composer.ts +8 -1
- package/src/data-checkup.ts +96 -7
- package/src/decay-engine.ts +59 -7
- package/src/distill-lock.ts +194 -15
- package/src/doctor.ts +13 -4
- package/src/dream-pipeline.ts +473 -41
- package/src/env-config.ts +118 -0
- package/src/error-signature.ts +153 -0
- package/src/error-taxonomy.ts +115 -0
- package/src/eval.ts +402 -14
- package/src/freshness.ts +192 -0
- package/src/http-request-policy.ts +148 -0
- package/src/idempotency.ts +8 -0
- package/src/ingest.ts +308 -12
- package/src/kg-extractor.ts +13 -3
- package/src/kg-store.ts +236 -44
- package/src/llm-client.ts +106 -11
- package/src/llm-consolidation.ts +2 -1
- package/src/mcp-server.ts +87 -1800
- package/src/mcp-tool-deps.ts +48 -0
- package/src/mcp-tools-advanced.ts +954 -0
- package/src/mcp-tools-core.ts +637 -0
- package/src/mcp-tools-governance.ts +475 -0
- package/src/memory-boundaries.ts +26 -3
- package/src/memory-evolution.ts +73 -1
- package/src/memory-health-rebalance.ts +4 -0
- package/src/memory-lint.ts +183 -10
- package/src/memory-output.ts +129 -4
- package/src/memory-promotion.ts +246 -0
- package/src/memory-schema.ts +25 -2
- package/src/memory-store-port.ts +46 -0
- package/src/multi-vector.ts +2 -1
- package/src/narrative-schema.ts +2 -1
- package/src/noise-filter.ts +57 -6
- package/src/noise-prototype-bank.ts +408 -0
- package/src/pii-detector.ts +207 -5
- package/src/pivot-apply.ts +399 -0
- package/src/preference-matcher.ts +37 -3
- package/src/query-expander.ts +90 -9
- package/src/recall-gate.ts +305 -0
- package/src/result-synthesizer.ts +2 -1
- package/src/retention-policy.ts +2 -1
- package/src/retrieval-profiles.ts +46 -4
- package/src/retriever.ts +354 -157
- package/src/retroactive-boost.ts +6 -2
- package/src/runtime-config.ts +78 -6
- package/src/scope-suggester.ts +113 -0
- package/src/session-distiller.ts +23 -2
- package/src/session-engine.ts +2 -1
- package/src/session-schema.ts +1 -0
- package/src/session-store.ts +5 -0
- package/src/skill-engine.ts +78 -0
- package/src/skill-promotion.ts +304 -48
- package/src/skill-schema.ts +16 -3
- package/src/skill-verifier.ts +152 -0
- package/src/store.ts +321 -57
- package/src/term-registry.ts +35 -0
- package/src/tool-output-compressor.ts +3 -1
- package/src/ui-server.ts +9 -1
- package/src/usage-tracker.ts +242 -0
- package/src/version-manager.ts +3 -2
- package/src/version.ts +19 -0
- package/src/workflow-observation-engine.ts +2 -1
- package/src/workflow-observation-schema.ts +2 -1
- package/src/workflow-observation-store.ts +6 -2
- package/.env +0 -4
- package/CLAUDE.md +0 -46
- package/config.json +0 -55
- package/daily-recall.sh +0 -110
- package/logs/daily-recall.error.log +0 -0
- package/logs/daily-recall.log +0 -60
- package/logs/ingest-2026-04-11.log +0 -110
- package/logs/ingest-2026-04-12.log +0 -90
- package/logs/ingest-2026-04-13.log +0 -87
- package/logs/launchagent-stderr.log +0 -0
- package/logs/launchagent-stdout.log +0 -0
package/src/retroactive-boost.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import type { MemoryStore, MemoryEntry } from "./store.js";
|
|
24
|
+
import { isActiveMemory } from "./memory-evolution.js";
|
|
24
25
|
|
|
25
26
|
// ---------------------------------------------------------------------------
|
|
26
27
|
// Config
|
|
@@ -85,12 +86,15 @@ export async function retroactiveBoost(
|
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
// Find related entries in the same scope
|
|
88
|
-
|
|
89
|
+
// Superseded belief-history rows come back from vectorSearch too. Boosting one is wasted
|
|
90
|
+
// — they are excluded from retrieval anyway — and it burns maxBoostPerTrigger budget that
|
|
91
|
+
// live entries should be getting.
|
|
92
|
+
const candidates = (await store.vectorSearch(
|
|
89
93
|
newEntry.vector,
|
|
90
94
|
config.maxBoostPerTrigger * 3, // fetch more, filter down
|
|
91
95
|
config.similarityThreshold,
|
|
92
96
|
[newEntry.scope],
|
|
93
|
-
);
|
|
97
|
+
)).filter((candidate) => isActiveMemory(candidate.entry.metadata));
|
|
94
98
|
|
|
95
99
|
const now = Date.now();
|
|
96
100
|
const minAgeMs = config.minAgeDays * 86_400_000;
|
package/src/runtime-config.ts
CHANGED
|
@@ -8,9 +8,11 @@ import { createEmbedder, getVectorDimensions, type EmbeddingConfig } from "./emb
|
|
|
8
8
|
import { createRetriever, type RetrievalConfig, DEFAULT_RETRIEVAL_CONFIG } from "./retriever.js";
|
|
9
9
|
import { applyRetrievalProfile } from "./retrieval-profiles.js";
|
|
10
10
|
import { AccessTracker } from "./access-tracker.js";
|
|
11
|
+
import { createAuditLogger } from "./audit-log.js";
|
|
11
12
|
import { FrequencyTracker } from "./frequency-tracker.js";
|
|
12
13
|
import { createLLMClient, type LLMClient, type LLMConfig } from "./llm-client.js";
|
|
13
14
|
import { logInfo } from "./stderr-log.js";
|
|
15
|
+
import * as envConfig from "./env-config.js";
|
|
14
16
|
|
|
15
17
|
export type RecallMode = "full" | "light" | "summary" | "off";
|
|
16
18
|
|
|
@@ -33,6 +35,12 @@ export interface LocalMemoryConfig {
|
|
|
33
35
|
};
|
|
34
36
|
sources: Record<string, { path: string; glob: string; description: string }>;
|
|
35
37
|
retrieval?: Partial<RetrievalConfig>;
|
|
38
|
+
/**
|
|
39
|
+
* Opt-in cross-scope sidecar map for search_memory.
|
|
40
|
+
* Main scoped search remains isolated; callers must pass includeRelatedScopes
|
|
41
|
+
* before configured related scopes are queried and shown separately.
|
|
42
|
+
*/
|
|
43
|
+
scopeRelations?: Record<string, string[]>;
|
|
36
44
|
/**
|
|
37
45
|
* Default depth for auto-recall injection.
|
|
38
46
|
* - "full": inject complete text (default, backward compatible)
|
|
@@ -60,7 +68,20 @@ export function loadDotEnv(): void {
|
|
|
60
68
|
}
|
|
61
69
|
|
|
62
70
|
export function findConfigPath(): string {
|
|
63
|
-
if (process.env.LOCAL_MEMORY_CONFIG)
|
|
71
|
+
if (process.env.LOCAL_MEMORY_CONFIG) {
|
|
72
|
+
const explicit = resolve(process.env.LOCAL_MEMORY_CONFIG);
|
|
73
|
+
// Check here rather than letting readFileSync throw a bare ENOENT: scripts
|
|
74
|
+
// resolve the database at module load, so a stale env var would otherwise
|
|
75
|
+
// surface as an unexplained crash before any of their own output appears.
|
|
76
|
+
if (!existsSync(explicit)) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`LOCAL_MEMORY_CONFIG points to a file that does not exist:\n` +
|
|
79
|
+
` ${explicit}\n` +
|
|
80
|
+
` Fix: correct the env var, or unset it to use the repo's config.json.`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return explicit;
|
|
84
|
+
}
|
|
64
85
|
|
|
65
86
|
const localConfig = resolve(metaDir(import.meta), "../config.json");
|
|
66
87
|
if (existsSync(localConfig)) return localConfig;
|
|
@@ -96,8 +117,35 @@ export function expandHome(p: string): string {
|
|
|
96
117
|
return p;
|
|
97
118
|
}
|
|
98
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Single source of truth for "which LanceDB directory do we operate on".
|
|
122
|
+
*
|
|
123
|
+
* Every entry point — MCP server, CLI, and maintenance scripts — must resolve
|
|
124
|
+
* the database through this function. Scripts that hardcode "./data/lancedb"
|
|
125
|
+
* or read a non-existent config key silently operate on a *different* database
|
|
126
|
+
* than the server does, so a health check, cleanup, or backfill can report on
|
|
127
|
+
* an empty directory while the real store sits elsewhere.
|
|
128
|
+
*
|
|
129
|
+
* Relative dbPath values resolve against the repo root (not process.cwd()), so
|
|
130
|
+
* a script's behaviour does not depend on where it was invoked from.
|
|
131
|
+
*
|
|
132
|
+
* Pass an already-loaded config where you have one. Calling this with no
|
|
133
|
+
* argument reads config.json from disk — convenient at a script's top level,
|
|
134
|
+
* but it means a missing or misconfigured file surfaces here, before the
|
|
135
|
+
* script's own output. findConfigPath() raises an actionable error for that.
|
|
136
|
+
*/
|
|
137
|
+
export function resolveDbPath(config?: LocalMemoryConfig): string {
|
|
138
|
+
const cfg = config ?? loadConfig();
|
|
139
|
+
return resolve(metaDir(import.meta), "..", expandHome(cfg.dbPath));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Sidecar directory (frequency-stats.json etc.) that lives beside the database. */
|
|
143
|
+
export function resolveDataDir(config?: LocalMemoryConfig): string {
|
|
144
|
+
return resolve(resolveDbPath(config), "..");
|
|
145
|
+
}
|
|
146
|
+
|
|
99
147
|
export function createComponents(config: LocalMemoryConfig, profileName?: string) {
|
|
100
|
-
const dbPath =
|
|
148
|
+
const dbPath = resolveDbPath(config);
|
|
101
149
|
validateStoragePath(dbPath);
|
|
102
150
|
|
|
103
151
|
const embeddingConfig: EmbeddingConfig = {
|
|
@@ -112,24 +160,48 @@ export function createComponents(config: LocalMemoryConfig, profileName?: string
|
|
|
112
160
|
|
|
113
161
|
const embedder = createEmbedder(embeddingConfig);
|
|
114
162
|
const store = new MemoryStore({ dbPath, vectorDim: embedder.dimensions });
|
|
115
|
-
const
|
|
163
|
+
const mergedRetrievalConfig = {
|
|
116
164
|
...DEFAULT_RETRIEVAL_CONFIG,
|
|
117
165
|
...(config.retrieval || {}),
|
|
118
166
|
};
|
|
167
|
+
// rerankApiKey 与 embedding.apiKey 同款支持 ${ENV_VAR} 引用。此前 retrieval 段是整体
|
|
168
|
+
// 展开、不过 resolveEnv,导致这个字段只能写明文——等于逼配置文件存凭证,是 rerank
|
|
169
|
+
// 一直没被启用的障碍之一。
|
|
170
|
+
const baseRetrievalConfig = mergedRetrievalConfig.rerankApiKey
|
|
171
|
+
? { ...mergedRetrievalConfig, rerankApiKey: resolveEnv(mergedRetrievalConfig.rerankApiKey) }
|
|
172
|
+
: mergedRetrievalConfig;
|
|
119
173
|
const { profile, config: retrieverConfig } = applyRetrievalProfile(baseRetrievalConfig, profileName);
|
|
120
174
|
const retriever = createRetriever(store, embedder, retrieverConfig);
|
|
121
175
|
|
|
122
176
|
// Attach access tracker for reinforcement-based decay
|
|
123
177
|
const accessTracker = new AccessTracker(store);
|
|
178
|
+
accessTracker.registerExitFlush();
|
|
124
179
|
retriever.setAccessTracker(accessTracker);
|
|
125
180
|
|
|
126
181
|
// P0.2: Attach frequency tracker for hit-count based boosting
|
|
127
|
-
const dataDir =
|
|
182
|
+
const dataDir = resolveDataDir(config);
|
|
128
183
|
const frequencyTracker = new FrequencyTracker({
|
|
129
184
|
filePath: join(dataDir, "frequency-stats.json"),
|
|
130
185
|
});
|
|
131
186
|
retriever.setFrequencyTracker(frequencyTracker);
|
|
132
187
|
|
|
188
|
+
// F-1: 补接 audit logger,让 retrieve 操作真的被记录。
|
|
189
|
+
//
|
|
190
|
+
// 2026-08-12 修复:`setAuditLogger`(retriever.ts:749)自定义以来**全仓零调用者**,
|
|
191
|
+
// 而 retriever.ts:897 的 `this.auditLogger?.log({ operation: "retrieve", ... })`
|
|
192
|
+
// 早就写好了 —— 可选链让它永远静默 no-op。结果是 audit-log.ts 文件头注释声称
|
|
193
|
+
// "record every store/update/delete/retrieve operation"、AuditOperation 类型里
|
|
194
|
+
// 也列着 "retrieve",而生产 audit.jsonl 里 2841 条记录**零条 retrieve**。
|
|
195
|
+
//
|
|
196
|
+
// 同族的 setAccessTracker / setFrequencyTracker 就在上面两行,都接了,唯独这个漏了。
|
|
197
|
+
// 与同日修的 dream 末尾 resetWriteCount 漏传 scope 完全同型:接线漏一处、运行时
|
|
198
|
+
// 不报错、静默失效 —— 差别只在那个是漏参数、这个是漏调用。
|
|
199
|
+
//
|
|
200
|
+
// simplified: audit.jsonl 无轮转机制(修复时 644K / 2841 行,约 230 字节一行)。
|
|
201
|
+
// 接上 retrieve 后增速会明显上升,因为检索远比写入频繁。
|
|
202
|
+
// 升级触发条件:文件超过约 50MB 时加按月轮转,别等到它拖慢 append。
|
|
203
|
+
retriever.setAuditLogger(createAuditLogger(join(dataDir, "audit.jsonl")));
|
|
204
|
+
|
|
133
205
|
// Create LLM client if configured (optional, graceful)
|
|
134
206
|
let llm: LLMClient | null = null;
|
|
135
207
|
if (config.llm) {
|
|
@@ -143,7 +215,7 @@ export function createComponents(config: LocalMemoryConfig, profileName?: string
|
|
|
143
215
|
}
|
|
144
216
|
|
|
145
217
|
export function createStoreOnly(config: LocalMemoryConfig): MemoryStore {
|
|
146
|
-
const dbPath =
|
|
218
|
+
const dbPath = resolveDbPath(config);
|
|
147
219
|
validateStoragePath(dbPath);
|
|
148
220
|
return new MemoryStore({
|
|
149
221
|
dbPath,
|
|
@@ -179,7 +251,7 @@ export function resolveRecallMode(config: LocalMemoryConfig, perCallOverride?: s
|
|
|
179
251
|
if (perCallOverride && VALID_RECALL_MODES.includes(perCallOverride as RecallMode)) {
|
|
180
252
|
return perCallOverride as RecallMode;
|
|
181
253
|
}
|
|
182
|
-
const envMode =
|
|
254
|
+
const envMode = envConfig.recallModeRaw();
|
|
183
255
|
if (envMode && VALID_RECALL_MODES.includes(envMode as RecallMode)) {
|
|
184
256
|
return envMode as RecallMode;
|
|
185
257
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* P1-A: scope 拼写漂移提示(FAILURES.md 5-13 retrieval gap)。
|
|
3
|
+
*
|
|
4
|
+
* matchesScopeFilter 走精确/前缀匹配,拼错或惯例不一致的 scope(`recallnst`、
|
|
5
|
+
* `cc_55bcbfb3`)会静默返回 0 hits,使用者无从知道是"真没有"还是"scope 写错"。
|
|
6
|
+
* 本模块在 0-hit 且显式传了 scope 时给出相近 scope 提示。
|
|
7
|
+
*
|
|
8
|
+
* 只提示、不自动 fallback 重查——自动改写会静默改变查询语义,agent/用户
|
|
9
|
+
* 看到提示后自行决定。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Pure matching
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
/** 归一化:小写 + 吃掉 -/_/空格(惯例差异:`cc-foo` vs `cc_foo` vs `ccfoo`)。 */
|
|
17
|
+
export function normalizeScope(s: string): string {
|
|
18
|
+
return s.toLowerCase().replace(/[-_\s]+/g, "");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Two-row Levenshtein with early exit — 距离超过 max 直接返回 max+1。 */
|
|
22
|
+
export function boundedLevenshtein(a: string, b: string, max: number): number {
|
|
23
|
+
if (Math.abs(a.length - b.length) > max) return max + 1;
|
|
24
|
+
if (a === b) return 0;
|
|
25
|
+
|
|
26
|
+
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
27
|
+
let curr = new Array<number>(b.length + 1);
|
|
28
|
+
|
|
29
|
+
for (let i = 1; i <= a.length; i++) {
|
|
30
|
+
curr[0] = i;
|
|
31
|
+
let rowMin = curr[0];
|
|
32
|
+
for (let j = 1; j <= b.length; j++) {
|
|
33
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
34
|
+
curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
|
|
35
|
+
if (curr[j] < rowMin) rowMin = curr[j];
|
|
36
|
+
}
|
|
37
|
+
if (rowMin > max) return max + 1;
|
|
38
|
+
[prev, curr] = [curr, prev];
|
|
39
|
+
}
|
|
40
|
+
return prev[b.length];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 对输入 scope 给出相近的已知 scope。
|
|
45
|
+
* 两级:① normalize 后精确相等 ② 前缀包含(双向)或编辑距离 ≤ 2。
|
|
46
|
+
* 候选应同时包含完整 scope 与其 prefix 家族(`cc:55bc...` 的 `cc`)——
|
|
47
|
+
* 拼错的通常是短 prefix,对长 session scope 做编辑距离没有意义。
|
|
48
|
+
*/
|
|
49
|
+
export function suggestScopes(input: string, knownScopes: string[], maxSuggestions = 3): string[] {
|
|
50
|
+
const norm = normalizeScope(input);
|
|
51
|
+
if (norm.length === 0) return [];
|
|
52
|
+
|
|
53
|
+
const exact: string[] = [];
|
|
54
|
+
const close: Array<{ scope: string; rank: number }> = [];
|
|
55
|
+
|
|
56
|
+
for (const known of knownScopes) {
|
|
57
|
+
const kn = normalizeScope(known);
|
|
58
|
+
if (kn === norm) {
|
|
59
|
+
exact.push(known);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (kn.startsWith(norm) || norm.startsWith(kn)) {
|
|
63
|
+
close.push({ scope: known, rank: 0.5 });
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const d = boundedLevenshtein(norm, kn, 2);
|
|
67
|
+
if (d <= 2) close.push({ scope: known, rank: d });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (exact.length > 0) return exact.slice(0, maxSuggestions);
|
|
71
|
+
return close
|
|
72
|
+
.sort((a, b) => a.rank - b.rank || a.scope.length - b.scope.length)
|
|
73
|
+
.map(c => c.scope)
|
|
74
|
+
.slice(0, maxSuggestions);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Cached provider
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 带 60s 缓存的建议器工厂——known scopes 来自 store.stats().scopeCounts,
|
|
83
|
+
* 全表聚合不便宜,0-hit 提示是低频路径,短缓存足够新鲜。
|
|
84
|
+
* 候选 = prefix 家族(冒号前段,去重) + 完整 scope 串。
|
|
85
|
+
*/
|
|
86
|
+
export function createScopeSuggester(
|
|
87
|
+
getScopeCounts: () => Promise<Record<string, number>>,
|
|
88
|
+
ttlMs = 60_000,
|
|
89
|
+
): (input: string) => Promise<string[]> {
|
|
90
|
+
let cache: { at: number; candidates: string[] } | null = null;
|
|
91
|
+
|
|
92
|
+
return async function suggest(input: string): Promise<string[]> {
|
|
93
|
+
const now = Date.now();
|
|
94
|
+
if (!cache || now - cache.at > ttlMs) {
|
|
95
|
+
const counts = await getScopeCounts();
|
|
96
|
+
const keys = Object.keys(counts);
|
|
97
|
+
const prefixes = new Set<string>();
|
|
98
|
+
for (const k of keys) {
|
|
99
|
+
const i = k.indexOf(":");
|
|
100
|
+
if (i > 0) prefixes.add(k.slice(0, i));
|
|
101
|
+
}
|
|
102
|
+
// prefix 家族在前:作为查询 scope 它们最常被手敲、也最常被拼错
|
|
103
|
+
cache = { at: now, candidates: [...prefixes, ...keys] };
|
|
104
|
+
}
|
|
105
|
+
return suggestScopes(input, cache.candidates);
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** 0-hit 提示行(空建议返回空串,caller 原样拼接即可)。 */
|
|
110
|
+
export function formatScopeSuggestion(inputScope: string, suggestions: string[]): string {
|
|
111
|
+
if (suggestions.length === 0) return "";
|
|
112
|
+
return `⚠️ scope '${inputScope}' 命中 0 条,相近 scope: [${suggestions.join(", ")}]`;
|
|
113
|
+
}
|
package/src/session-distiller.ts
CHANGED
|
@@ -7,6 +7,8 @@ import type { AuditLogger } from "./audit-log.js";
|
|
|
7
7
|
import type { StoredMemoryRecord, DurableMemoryCategory } from "./memory-schema.js";
|
|
8
8
|
import { persistMemory } from "./capture-engine.js";
|
|
9
9
|
import { detectLang, getSessionPromptHook } from "./language-hook.js";
|
|
10
|
+
import { redactSecrets } from "./pii-detector.js";
|
|
11
|
+
import { withWriteLock } from "./distill-lock.js";
|
|
10
12
|
|
|
11
13
|
// ============================================================================
|
|
12
14
|
// Types
|
|
@@ -71,6 +73,7 @@ const DEFAULT_DIMENSION_LABELS: Record<string, string> = {
|
|
|
71
73
|
errors_and_fixes: "Errors and fix records",
|
|
72
74
|
problem_solving: "Problem solving process",
|
|
73
75
|
user_quotes: "User original quotes preserved",
|
|
76
|
+
user_profile: "Stable facts about the user: identity, role, background, long-term context",
|
|
74
77
|
unfinished_tasks: "Unfinished tasks",
|
|
75
78
|
current_state: "Current work state",
|
|
76
79
|
next_steps: "Suggested next steps",
|
|
@@ -93,6 +96,15 @@ const DIMENSION_TO_MEMORY: Record<string, DimensionMapping> = {
|
|
|
93
96
|
errors_and_fixes: { category: "cases", importance: 0.7 },
|
|
94
97
|
problem_solving: { category: "patterns", importance: 0.8 },
|
|
95
98
|
user_quotes: { category: "preferences", importance: 0.7 },
|
|
99
|
+
// user_profile → profile:让 distill 能沉淀"用户是谁"的稳定身份事实(不只是偏好)。
|
|
100
|
+
// distill 走 source=agent,不被 resolveIngestBoundary 降级,直接进 durable profile。
|
|
101
|
+
user_profile: { category: "profile", importance: 0.8 },
|
|
102
|
+
// 2026-05-13 P1.2: 加 unfinished_tasks + next_steps 持久化映射
|
|
103
|
+
// LLM 早在 DEFAULT_DIMENSION_LABELS 行 74/76 已产出这两条,但缺映射被丢弃
|
|
104
|
+
// 都映射到 cases/patterns 走 tag 兜底(extractAndPersist 自动加 tags: ["session_distill", dimKey])
|
|
105
|
+
// 未来如要 unresolved-first 召回,按 tags 含 "unfinished_tasks" 过滤即可
|
|
106
|
+
unfinished_tasks: { category: "cases", importance: 0.75 },
|
|
107
|
+
next_steps: { category: "patterns", importance: 0.65 },
|
|
96
108
|
};
|
|
97
109
|
|
|
98
110
|
// ============================================================================
|
|
@@ -158,8 +170,10 @@ export async function summarizeSession(
|
|
|
158
170
|
messages: ConversationMessage[],
|
|
159
171
|
llm: LLMClient,
|
|
160
172
|
): Promise<SummaryResult> {
|
|
173
|
+
// F-3b: Redact per-message BEFORE the 500-char cut — truncating first could
|
|
174
|
+
// slice a secret in half and leak its prefix past the redaction regexes.
|
|
161
175
|
const conversationText = messages
|
|
162
|
-
.map((m) => `[${m.role}${m.tool_name ? `:${m.tool_name}` : ""}] ${m.content.slice(0, 500)}`)
|
|
176
|
+
.map((m) => `[${m.role}${m.tool_name ? `:${m.tool_name}` : ""}] ${redactSecrets(m.content).text.slice(0, 500)}`)
|
|
163
177
|
.join("\n")
|
|
164
178
|
.slice(0, 8000);
|
|
165
179
|
|
|
@@ -295,7 +309,14 @@ export async function distillSession(
|
|
|
295
309
|
};
|
|
296
310
|
|
|
297
311
|
if (shouldPersist && Object.keys(summaryResult.dimensions).length > 0) {
|
|
298
|
-
|
|
312
|
+
// P0-1: serialize the persist phase per scope across processes. Wait (not skip) —
|
|
313
|
+
// skipping would drop this session's distilled knowledge when two sessions distill
|
|
314
|
+
// the same scope at once. Writes are fast and store-write already makes each write
|
|
315
|
+
// idempotent, so the brief wait is lossless; microcompact/summary above run unlocked.
|
|
316
|
+
persistResult = await withWriteLock(
|
|
317
|
+
`distill-${scope}`,
|
|
318
|
+
() => extractAndPersist(summaryResult, deps, scope),
|
|
319
|
+
);
|
|
299
320
|
}
|
|
300
321
|
|
|
301
322
|
return {
|
package/src/session-engine.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type SessionCheckpointRecord,
|
|
7
7
|
SessionCheckpointRecordSchema,
|
|
8
8
|
} from "./session-schema.js";
|
|
9
|
+
import { buildIdempotentRecordId } from "./idempotency.js";
|
|
9
10
|
|
|
10
11
|
const REPO_STATE_PATTERN = /\bgit status\b|未提交|staged|uncommitted|已修改文件|modified files?|untracked|新增文件|dirty repo|dirty worktree/iu;
|
|
11
12
|
const SENTENCE_SPLIT_PATTERN = /(?:\r?\n)+|(?<=[。!?!?;;])\s+/u;
|
|
@@ -141,7 +142,7 @@ export function buildSessionCheckpointResult(rawInput: unknown): SessionCheckpoi
|
|
|
141
142
|
|
|
142
143
|
const record = SessionCheckpointRecordSchema.parse({
|
|
143
144
|
...sanitizedInput,
|
|
144
|
-
checkpointId: randomUUID(),
|
|
145
|
+
checkpointId: buildIdempotentRecordId("chk", sanitizedInput.idempotencyKey) ?? randomUUID(),
|
|
145
146
|
resolvedScope: resolveCheckpointScope(sanitizedInput),
|
|
146
147
|
});
|
|
147
148
|
|
package/src/session-schema.ts
CHANGED
|
@@ -14,6 +14,7 @@ export const SessionCheckpointInputSchema = z.object({
|
|
|
14
14
|
nextActions: normalizedStringListSchema("nextActions", 6, 200),
|
|
15
15
|
entities: normalizedStringListSchema("entities", 8, 120),
|
|
16
16
|
files: normalizedStringListSchema("files", 12, 220),
|
|
17
|
+
idempotencyKey: optionalBoundedStringSchema(160),
|
|
17
18
|
updatedAt: z.string().datetime().default(() => new Date().toISOString()),
|
|
18
19
|
});
|
|
19
20
|
|
package/src/session-store.ts
CHANGED
|
@@ -78,6 +78,11 @@ export class SessionCheckpointStore {
|
|
|
78
78
|
const timestampToken = parsed.updatedAt.replace(/[:.]/g, "-");
|
|
79
79
|
const path = join(this.dataDir, `${timestampToken}-${parsed.checkpointId}.json`);
|
|
80
80
|
try {
|
|
81
|
+
const suffix = `-${parsed.checkpointId}.json`;
|
|
82
|
+
for (const name of readdirSync(this.dataDir).filter((item) => item.endsWith(suffix))) {
|
|
83
|
+
const existingPath = join(this.dataDir, name);
|
|
84
|
+
if (existingPath !== path) unlinkSync(existingPath);
|
|
85
|
+
}
|
|
81
86
|
writeFileSync(path, JSON.stringify(parsed, null, 2) + "\n");
|
|
82
87
|
} catch (err) {
|
|
83
88
|
console.error("[recallnest] Failed to save session checkpoint:", err instanceof Error ? err.message : String(err));
|
package/src/skill-engine.ts
CHANGED
|
@@ -177,6 +177,84 @@ export async function persistSkill(
|
|
|
177
177
|
};
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
type OutcomeDeps = Pick<MemoryStore, "getById" | "update">;
|
|
181
|
+
|
|
182
|
+
export type SkillOutcomeUpdate =
|
|
183
|
+
| { updated: true; successCount: number; failureCount: number; lastRefinedAt: string }
|
|
184
|
+
| { updated: false; reason: "skill_not_found" | "not_a_skill" | "metadata_parse_error" | "skill_metadata_missing" | "store_update_failed" };
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Record a skill usage outcome by incrementing successCount/failureCount on the skill's metadata.
|
|
188
|
+
*
|
|
189
|
+
* Mapping: `success` → successCount +1; everything else (`failure`/`corrected`/`missed`) → failureCount +1.
|
|
190
|
+
* Returns `{ updated: false, reason }` for missing/corrupt skills without throwing — callers can log and continue.
|
|
191
|
+
*/
|
|
192
|
+
export async function recordSkillOutcome(
|
|
193
|
+
store: OutcomeDeps,
|
|
194
|
+
skillId: string,
|
|
195
|
+
outcome: "success" | "failure" | "corrected" | "missed",
|
|
196
|
+
): Promise<SkillOutcomeUpdate> {
|
|
197
|
+
const entry = await store.getById(skillId);
|
|
198
|
+
if (!entry) {
|
|
199
|
+
return { updated: false, reason: "skill_not_found" };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (entry.category !== SKILL_CATEGORY) {
|
|
203
|
+
return { updated: false, reason: "not_a_skill" };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
let meta: Record<string, unknown>;
|
|
207
|
+
try {
|
|
208
|
+
meta = JSON.parse(entry.metadata || "{}");
|
|
209
|
+
} catch {
|
|
210
|
+
return { updated: false, reason: "metadata_parse_error" };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const skill = meta.skill;
|
|
214
|
+
if (!skill || typeof skill !== "object") {
|
|
215
|
+
return { updated: false, reason: "skill_metadata_missing" };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const s = skill as Record<string, unknown>;
|
|
219
|
+
const currentSuccess = typeof s.successCount === "number" ? s.successCount : 0;
|
|
220
|
+
const currentFailure = typeof s.failureCount === "number" ? s.failureCount : 0;
|
|
221
|
+
|
|
222
|
+
const nextSuccessCount = outcome === "success" ? currentSuccess + 1 : currentSuccess;
|
|
223
|
+
const nextFailureCount = outcome === "success" ? currentFailure : currentFailure + 1;
|
|
224
|
+
const nextLastRefinedAt = new Date().toISOString();
|
|
225
|
+
|
|
226
|
+
const nextSkill = {
|
|
227
|
+
...s,
|
|
228
|
+
successCount: nextSuccessCount,
|
|
229
|
+
failureCount: nextFailureCount,
|
|
230
|
+
lastRefinedAt: nextLastRefinedAt,
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const updatedMetadata = JSON.stringify({
|
|
234
|
+
...meta,
|
|
235
|
+
skill: nextSkill,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Use entry.id (full UUID resolved by getById's prefix lookup) for update,
|
|
239
|
+
// not skillId (caller-provided, may be 8+ hex prefix). This avoids ambiguous-prefix
|
|
240
|
+
// mutation risk: if a prefix matched here, update would also need to do prefix
|
|
241
|
+
// resolution, doubling the chance of writing to the wrong entry.
|
|
242
|
+
const updated = await store.update(entry.id, {
|
|
243
|
+
metadata: updatedMetadata,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
if (!updated) {
|
|
247
|
+
return { updated: false, reason: "store_update_failed" };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
updated: true,
|
|
252
|
+
successCount: nextSuccessCount,
|
|
253
|
+
failureCount: nextFailureCount,
|
|
254
|
+
lastRefinedAt: nextLastRefinedAt,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
180
258
|
export async function retrieveSkills(
|
|
181
259
|
store: SearchDeps,
|
|
182
260
|
embedder: EmbedDeps,
|