recallnest 2.5.4 → 2.6.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/CHANGELOG.md +37 -0
- package/README.md +34 -9
- package/README_CN.md +37 -12
- package/package.json +6 -5
- package/src/__tests__/access-tracker-novelty.test.ts +66 -0
- package/src/__tests__/api-server-security.test.ts +102 -0
- package/src/__tests__/belief-history.test.ts +220 -0
- package/src/__tests__/capture-engine.test.ts +93 -3
- package/src/__tests__/category-min-scores.test.ts +83 -0
- package/src/__tests__/cluster-consolidation.test.ts +167 -4
- package/src/__tests__/conflict-store.test.ts +46 -2
- package/src/__tests__/consolidation-engine.test.ts +97 -0
- package/src/__tests__/contracts.test.ts +46 -0
- package/src/__tests__/decay-exemption.test.ts +45 -0
- package/src/__tests__/dream-pipeline.test.ts +464 -5
- package/src/__tests__/freshness.test.ts +186 -0
- package/src/__tests__/hotness-blend.test.ts +207 -0
- package/src/__tests__/llm-client-pivot.test.ts +628 -0
- package/src/__tests__/maintenance-locks.test.ts +15 -0
- package/src/__tests__/memory-boundaries.test.ts +32 -0
- package/src/__tests__/noise-prototype-bank.test.ts +280 -0
- package/src/__tests__/pii-redact.test.ts +19 -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__/preference-matcher.test.ts +54 -2
- package/src/__tests__/pull-from-macbook-sync-only.test.ts +679 -0
- package/src/__tests__/recall-gate.test.ts +266 -0
- package/src/__tests__/resolve-db-path.test.ts +130 -0
- package/src/__tests__/resolve-tier.test.ts +64 -0
- package/src/__tests__/retrieval-eval.test.ts +163 -0
- package/src/__tests__/retriever-lazy-init-dispatch.test.ts +123 -0
- package/src/__tests__/skill-promotion.test.ts +53 -0
- package/src/__tests__/stable-context-length-contract.test.ts +62 -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-upsert.test.ts +17 -0
- package/src/__tests__/term-registry-terminal-roster.test.ts +127 -0
- package/src/__tests__/ui-server-security.test.ts +93 -0
- package/src/access-tracker.ts +53 -3
- package/src/adaptive-retrieval.ts +11 -0
- package/src/api-server.ts +9 -2
- package/src/auto-recall.ts +61 -1
- package/src/belief-history.ts +164 -0
- package/src/capture-engine.ts +88 -11
- package/src/cli.ts +109 -14
- package/src/conflict-engine.ts +24 -3
- package/src/consolidation-engine.ts +146 -12
- package/src/context-composer-stable-selection.ts +7 -1
- package/src/data-checkup.ts +1 -1
- package/src/decay-engine.ts +59 -7
- package/src/doctor.ts +4 -2
- package/src/dream-pipeline.ts +359 -11
- package/src/env-config.ts +41 -0
- package/src/eval.ts +154 -11
- package/src/freshness.ts +192 -0
- package/src/http-request-policy.ts +148 -0
- package/src/ingest.ts +277 -2
- package/src/kg-extractor.ts +7 -1
- package/src/llm-client.ts +106 -11
- package/src/mcp-server.ts +2 -1
- package/src/mcp-tools-advanced.ts +13 -0
- package/src/mcp-tools-core.ts +5 -1
- package/src/memory-boundaries.ts +23 -2
- package/src/memory-health-rebalance.ts +4 -0
- package/src/memory-output.ts +16 -2
- package/src/memory-store-port.ts +3 -0
- package/src/noise-prototype-bank.ts +408 -0
- package/src/pii-detector.ts +52 -2
- package/src/pivot-apply.ts +399 -0
- package/src/preference-matcher.ts +37 -3
- package/src/recall-gate.ts +305 -0
- package/src/retrieval-profiles.ts +46 -4
- package/src/retriever.ts +95 -18
- package/src/retroactive-boost.ts +6 -2
- package/src/runtime-config.ts +70 -5
- package/src/skill-promotion.ts +58 -2
- package/src/store.ts +54 -19
- package/src/term-registry.ts +35 -0
- package/src/ui-server.ts +7 -0
- package/src/version.ts +19 -0
package/src/runtime-config.ts
CHANGED
|
@@ -8,6 +8,7 @@ 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";
|
|
@@ -67,7 +68,20 @@ export function loadDotEnv(): void {
|
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
export function findConfigPath(): string {
|
|
70
|
-
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
|
+
}
|
|
71
85
|
|
|
72
86
|
const localConfig = resolve(metaDir(import.meta), "../config.json");
|
|
73
87
|
if (existsSync(localConfig)) return localConfig;
|
|
@@ -103,8 +117,35 @@ export function expandHome(p: string): string {
|
|
|
103
117
|
return p;
|
|
104
118
|
}
|
|
105
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
|
+
|
|
106
147
|
export function createComponents(config: LocalMemoryConfig, profileName?: string) {
|
|
107
|
-
const dbPath =
|
|
148
|
+
const dbPath = resolveDbPath(config);
|
|
108
149
|
validateStoragePath(dbPath);
|
|
109
150
|
|
|
110
151
|
const embeddingConfig: EmbeddingConfig = {
|
|
@@ -119,24 +160,48 @@ export function createComponents(config: LocalMemoryConfig, profileName?: string
|
|
|
119
160
|
|
|
120
161
|
const embedder = createEmbedder(embeddingConfig);
|
|
121
162
|
const store = new MemoryStore({ dbPath, vectorDim: embedder.dimensions });
|
|
122
|
-
const
|
|
163
|
+
const mergedRetrievalConfig = {
|
|
123
164
|
...DEFAULT_RETRIEVAL_CONFIG,
|
|
124
165
|
...(config.retrieval || {}),
|
|
125
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;
|
|
126
173
|
const { profile, config: retrieverConfig } = applyRetrievalProfile(baseRetrievalConfig, profileName);
|
|
127
174
|
const retriever = createRetriever(store, embedder, retrieverConfig);
|
|
128
175
|
|
|
129
176
|
// Attach access tracker for reinforcement-based decay
|
|
130
177
|
const accessTracker = new AccessTracker(store);
|
|
178
|
+
accessTracker.registerExitFlush();
|
|
131
179
|
retriever.setAccessTracker(accessTracker);
|
|
132
180
|
|
|
133
181
|
// P0.2: Attach frequency tracker for hit-count based boosting
|
|
134
|
-
const dataDir =
|
|
182
|
+
const dataDir = resolveDataDir(config);
|
|
135
183
|
const frequencyTracker = new FrequencyTracker({
|
|
136
184
|
filePath: join(dataDir, "frequency-stats.json"),
|
|
137
185
|
});
|
|
138
186
|
retriever.setFrequencyTracker(frequencyTracker);
|
|
139
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
|
+
|
|
140
205
|
// Create LLM client if configured (optional, graceful)
|
|
141
206
|
let llm: LLMClient | null = null;
|
|
142
207
|
if (config.llm) {
|
|
@@ -150,7 +215,7 @@ export function createComponents(config: LocalMemoryConfig, profileName?: string
|
|
|
150
215
|
}
|
|
151
216
|
|
|
152
217
|
export function createStoreOnly(config: LocalMemoryConfig): MemoryStore {
|
|
153
|
-
const dbPath =
|
|
218
|
+
const dbPath = resolveDbPath(config);
|
|
154
219
|
validateStoragePath(dbPath);
|
|
155
220
|
return new MemoryStore({
|
|
156
221
|
dbPath,
|
package/src/skill-promotion.ts
CHANGED
|
@@ -28,6 +28,8 @@ export interface PromotionCandidate {
|
|
|
28
28
|
confidence: number; // 0-1
|
|
29
29
|
/** A2: zero-LLM verifier verdict (pattern_to_skill only). 失败候选保留披露、排后、不静默丢。 */
|
|
30
30
|
verification?: VerifyResult;
|
|
31
|
+
/** Read-signal evidence aggregated from source entries(Artel 读驱动升华:被真实读过的候选提权)。 */
|
|
32
|
+
readEvidence?: { totalAccess: number; distinctReaders: number };
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
export interface PromotionScanResult {
|
|
@@ -266,6 +268,47 @@ export function greedyCluster(
|
|
|
266
268
|
return clusters;
|
|
267
269
|
}
|
|
268
270
|
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
// Read evidence (Artel-style read-driven promotion)
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
/** Max confidence boost from read evidence. Saturates at 4 distinct readers. */
|
|
276
|
+
const READ_BOOST_MAX = 0.15;
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Aggregate read signals (accessCount / readerIds / distinctReaderCount, written
|
|
280
|
+
* by AccessTracker on retrieval) across a candidate's source entries.
|
|
281
|
+
* distinctReaders = max(union of readerIds, per-entry saturated count) — the
|
|
282
|
+
* union undercounts once READER_ID_CAP saturates, the per-entry count covers that.
|
|
283
|
+
*/
|
|
284
|
+
export function aggregateReadEvidence(
|
|
285
|
+
entries: Array<{ metadata?: string }>,
|
|
286
|
+
): { totalAccess: number; distinctReaders: number } {
|
|
287
|
+
let totalAccess = 0;
|
|
288
|
+
const readers = new Set<string>();
|
|
289
|
+
let saturatedMax = 0;
|
|
290
|
+
for (const e of entries) {
|
|
291
|
+
try {
|
|
292
|
+
const meta = JSON.parse(e.metadata || "{}") as Record<string, unknown>;
|
|
293
|
+
if (typeof meta.accessCount === "number") totalAccess += meta.accessCount;
|
|
294
|
+
if (Array.isArray(meta.readerIds)) {
|
|
295
|
+
for (const r of meta.readerIds) if (typeof r === "string") readers.add(r);
|
|
296
|
+
}
|
|
297
|
+
if (typeof meta.distinctReaderCount === "number") {
|
|
298
|
+
saturatedMax = Math.max(saturatedMax, meta.distinctReaderCount);
|
|
299
|
+
}
|
|
300
|
+
} catch { /* broken metadata → contributes nothing */ }
|
|
301
|
+
}
|
|
302
|
+
return { totalAccess, distinctReaders: Math.max(readers.size, saturatedMax) };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Blend read evidence into base confidence: +0..READ_BOOST_MAX, capped at 0.99.
|
|
306
|
+
* 提权不设门槛(observe-before-enforce:先积累读证据,硬门槛等有生产数据再定)。 */
|
|
307
|
+
export function applyReadBoost(base: number, distinctReaders: number): number {
|
|
308
|
+
const boost = Math.min(1, distinctReaders / 4) * READ_BOOST_MAX;
|
|
309
|
+
return Math.min(0.99, base + boost);
|
|
310
|
+
}
|
|
311
|
+
|
|
269
312
|
// ---------------------------------------------------------------------------
|
|
270
313
|
// Main scan
|
|
271
314
|
// ---------------------------------------------------------------------------
|
|
@@ -339,6 +382,7 @@ export async function scanForPromotions(
|
|
|
339
382
|
const avgSim = computeAverageIntraClusterSimilarity(cluster.members);
|
|
340
383
|
|
|
341
384
|
caseCandidates++;
|
|
385
|
+
const caseReadEvidence = aggregateReadEvidence(cluster.members);
|
|
342
386
|
result.candidates.push({
|
|
343
387
|
type: "case_to_pattern",
|
|
344
388
|
sourceEntries: cluster.members.map(m => ({
|
|
@@ -348,7 +392,11 @@ export async function scanForPromotions(
|
|
|
348
392
|
})),
|
|
349
393
|
suggestedName: extractName(cluster.seed.text),
|
|
350
394
|
suggestedDescription: summarizeCluster(cluster.members),
|
|
351
|
-
confidence:
|
|
395
|
+
confidence: applyReadBoost(
|
|
396
|
+
cluster.members.length / (cluster.members.length + 2), // Bayesian smoothing
|
|
397
|
+
caseReadEvidence.distinctReaders,
|
|
398
|
+
),
|
|
399
|
+
readEvidence: caseReadEvidence,
|
|
352
400
|
});
|
|
353
401
|
}
|
|
354
402
|
}
|
|
@@ -394,6 +442,7 @@ export async function scanForPromotions(
|
|
|
394
442
|
);
|
|
395
443
|
|
|
396
444
|
patternCandidates++;
|
|
445
|
+
const patternReadEvidence = aggregateReadEvidence([pattern, ...similarCases.map(h => h.entry)]);
|
|
397
446
|
result.candidates.push({
|
|
398
447
|
type: "pattern_to_skill",
|
|
399
448
|
sourceEntries: [
|
|
@@ -407,8 +456,12 @@ export async function scanForPromotions(
|
|
|
407
456
|
suggestedName: extractName(pattern.text),
|
|
408
457
|
suggestedDescription: `Skill derived from pattern with ${similarCases.length} supporting cases`,
|
|
409
458
|
suggestedImplementation: extractSteps(pattern.text),
|
|
410
|
-
confidence:
|
|
459
|
+
confidence: applyReadBoost(
|
|
460
|
+
similarCases.length / (similarCases.length + 2),
|
|
461
|
+
patternReadEvidence.distinctReaders,
|
|
462
|
+
),
|
|
411
463
|
verification,
|
|
464
|
+
readEvidence: patternReadEvidence,
|
|
412
465
|
});
|
|
413
466
|
}
|
|
414
467
|
|
|
@@ -455,6 +508,9 @@ export function formatPromotionResult(result: PromotionScanResult): string {
|
|
|
455
508
|
lines.push(`Confidence: ${(c.confidence * 100).toFixed(1)}%`);
|
|
456
509
|
lines.push(`Description: ${c.suggestedDescription}`);
|
|
457
510
|
lines.push(`Sources: ${c.sourceEntries.length} entries`);
|
|
511
|
+
if (c.readEvidence) {
|
|
512
|
+
lines.push(`Read evidence: ${c.readEvidence.totalAccess} total reads, ${c.readEvidence.distinctReaders} distinct reader(s)`);
|
|
513
|
+
}
|
|
458
514
|
if (c.verification) {
|
|
459
515
|
const v = c.verification;
|
|
460
516
|
if (v.ok) {
|
package/src/store.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
7
7
|
import { existsSync, accessSync, constants, mkdirSync, realpathSync, lstatSync } from "node:fs";
|
|
8
8
|
import { dirname, join } from "node:path";
|
|
9
9
|
import { logWarn } from "./stderr-log.js";
|
|
10
|
+
import { readConsistencyInterval as envReadConsistencyInterval } from "./env-config.js";
|
|
10
11
|
import type { DurableMemoryCategory } from "./memory-schema.js";
|
|
11
12
|
import { matchesScopeFilter } from "./scope-policy.js";
|
|
12
13
|
import { detectEmotionIfEnabled } from "./emotion-detector.js";
|
|
@@ -58,6 +59,13 @@ export interface MemorySearchResult {
|
|
|
58
59
|
export interface StoreConfig {
|
|
59
60
|
dbPath: string;
|
|
60
61
|
vectorDim: number;
|
|
62
|
+
/**
|
|
63
|
+
* LanceDB readConsistencyInterval (seconds): 0 = strong consistency (external
|
|
64
|
+
* writes visible on every read), positive = bounded staleness window.
|
|
65
|
+
* Omitted → RECALLNEST_READ_CONSISTENCY_INTERVAL decides (default 0; "off"
|
|
66
|
+
* restores the legacy unchecked-handle behavior).
|
|
67
|
+
*/
|
|
68
|
+
readConsistencyInterval?: number;
|
|
61
69
|
}
|
|
62
70
|
|
|
63
71
|
export type LegacyScopeIssueKind = "missing" | "empty" | "global";
|
|
@@ -222,6 +230,9 @@ export class MemoryStore implements MemoryStorePort {
|
|
|
222
230
|
/** Per-id serial queues for patchMetadata; entries self-remove on settle. */
|
|
223
231
|
private readonly metadataPatchQueues = new Map<string, Promise<void>>();
|
|
224
232
|
|
|
233
|
+
/** Set when the init-time schema migration fails; surfaced in store error messages. */
|
|
234
|
+
private schemaMigrationError: string | null = null;
|
|
235
|
+
|
|
225
236
|
constructor(private readonly config: StoreConfig) {}
|
|
226
237
|
|
|
227
238
|
get dbPath(): string {
|
|
@@ -248,7 +259,13 @@ export class MemoryStore implements MemoryStorePort {
|
|
|
248
259
|
|
|
249
260
|
let db: LanceDB.Connection;
|
|
250
261
|
try {
|
|
251
|
-
|
|
262
|
+
// Without a consistency interval a long-lived handle pins its manifest
|
|
263
|
+
// version — resident MCP/API/UI servers would never see CLI or
|
|
264
|
+
// sibling-process writes. Explicit config wins so tests can pin behavior.
|
|
265
|
+
db = await lancedb.connect(this.config.dbPath, {
|
|
266
|
+
readConsistencyInterval:
|
|
267
|
+
this.config.readConsistencyInterval ?? envReadConsistencyInterval(),
|
|
268
|
+
});
|
|
252
269
|
} catch (err: any) {
|
|
253
270
|
const code = err.code || "";
|
|
254
271
|
const message = err.message || String(err);
|
|
@@ -266,24 +283,24 @@ export class MemoryStore implements MemoryStorePort {
|
|
|
266
283
|
try {
|
|
267
284
|
table = await db.openTable(TABLE_NAME);
|
|
268
285
|
|
|
269
|
-
// Backward compatibility: add missing columns to existing tables
|
|
286
|
+
// Backward compatibility: add missing columns to existing tables.
|
|
287
|
+
// Read the schema definition, not sample rows — a row-sampling check can
|
|
288
|
+
// never fire on a 0-row table, which deadlocked every write with
|
|
289
|
+
// "Found field not in schema" on empty pre-language/fts_text tables.
|
|
270
290
|
try {
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
280
|
-
if (!("fts_text" in sample[0])) {
|
|
281
|
-
logWarn("Adding fts_text column for backward compatibility");
|
|
282
|
-
await table.addColumns([{ name: "fts_text", valueSql: "text" }]);
|
|
283
|
-
}
|
|
291
|
+
const schema = await table.schema();
|
|
292
|
+
const fields = new Set(schema.fields.map((f) => f.name));
|
|
293
|
+
const missing: Array<{ name: string; valueSql: string }> = [];
|
|
294
|
+
if (!fields.has("language")) missing.push({ name: "language", valueSql: "'en'" });
|
|
295
|
+
if (!fields.has("fts_text")) missing.push({ name: "fts_text", valueSql: "text" });
|
|
296
|
+
if (missing.length > 0) {
|
|
297
|
+
logWarn(`Adding missing columns for backward compatibility: ${missing.map((c) => c.name).join(", ")}`);
|
|
298
|
+
await table.addColumns(missing);
|
|
284
299
|
}
|
|
300
|
+
this.schemaMigrationError = null;
|
|
285
301
|
} catch (err) {
|
|
286
|
-
|
|
302
|
+
this.schemaMigrationError = err instanceof Error ? err.message : String(err);
|
|
303
|
+
logWarn("Could not check/migrate table schema — subsequent writes may fail:", err);
|
|
287
304
|
}
|
|
288
305
|
} catch (_openErr) {
|
|
289
306
|
// Table doesn't exist yet — create it
|
|
@@ -388,7 +405,8 @@ export class MemoryStore implements MemoryStorePort {
|
|
|
388
405
|
const code = err.code || "";
|
|
389
406
|
const message = err.message || String(err);
|
|
390
407
|
throw new Error(
|
|
391
|
-
`Failed to store memory in "${this.config.dbPath}": ${code} ${message}`
|
|
408
|
+
`Failed to store memory in "${this.config.dbPath}": ${code} ${message}` +
|
|
409
|
+
(this.schemaMigrationError ? `\n Schema migration failed at init: ${this.schemaMigrationError}` : "")
|
|
392
410
|
);
|
|
393
411
|
}
|
|
394
412
|
}
|
|
@@ -440,7 +458,8 @@ export class MemoryStore implements MemoryStorePort {
|
|
|
440
458
|
const code = err.code || "";
|
|
441
459
|
const message = err.message || String(err);
|
|
442
460
|
throw new Error(
|
|
443
|
-
`Failed to batch store ${entries.length} memories in "${this.config.dbPath}": ${code} ${message}`
|
|
461
|
+
`Failed to batch store ${entries.length} memories in "${this.config.dbPath}": ${code} ${message}` +
|
|
462
|
+
(this.schemaMigrationError ? `\n Schema migration failed at init: ${this.schemaMigrationError}` : "")
|
|
444
463
|
);
|
|
445
464
|
}
|
|
446
465
|
// Per-scope write counts drive dream activation. Best-effort — never block writes.
|
|
@@ -1065,7 +1084,10 @@ export class MemoryStore implements MemoryStorePort {
|
|
|
1065
1084
|
const updated: MemoryEntry = {
|
|
1066
1085
|
id: row.id as string,
|
|
1067
1086
|
text: updates.text ?? (row.text as string),
|
|
1068
|
-
|
|
1087
|
+
// row.vector 可能为 null(历史病行,2026-07-23 dream/auto-gc 生产实锤:
|
|
1088
|
+
// cc:82a919ea 连崩三轮 Array.from(null))。null → [] 让 metadata-only update
|
|
1089
|
+
// 不再被病行绊倒;空向量本就不可检索,语义不变。
|
|
1090
|
+
vector: updates.vector ?? (row.vector ? Array.from(row.vector as Iterable<number>) : []),
|
|
1069
1091
|
category: updates.category ?? (row.category as MemoryEntry["category"]),
|
|
1070
1092
|
scope: rowScope,
|
|
1071
1093
|
importance: updates.importance ?? Number(row.importance),
|
|
@@ -1212,6 +1234,19 @@ export class MemoryStore implements MemoryStorePort {
|
|
|
1212
1234
|
return deleteCount;
|
|
1213
1235
|
}
|
|
1214
1236
|
|
|
1237
|
+
/**
|
|
1238
|
+
* Force lazy initialization to complete.
|
|
1239
|
+
*
|
|
1240
|
+
* `hasFtsSupport` is meaningless before init: `ftsIndexCreated` starts false and
|
|
1241
|
+
* only flips once `doInitialize()` has attempted to build the FTS index. Any caller
|
|
1242
|
+
* that *branches* on `hasFtsSupport` must await this first, or it will read the
|
|
1243
|
+
* pre-init false and take the wrong branch. Cheap to call repeatedly —
|
|
1244
|
+
* `ensureInitialized()` returns immediately once the table exists.
|
|
1245
|
+
*/
|
|
1246
|
+
async ready(): Promise<void> {
|
|
1247
|
+
await this.ensureInitialized();
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1215
1250
|
get hasFtsSupport(): boolean {
|
|
1216
1251
|
return this.ftsIndexCreated;
|
|
1217
1252
|
}
|
package/src/term-registry.ts
CHANGED
|
@@ -272,12 +272,24 @@ export const TASK_RESULT_SPECIFICITY_GROUPS = [
|
|
|
272
272
|
"managed continuity rules",
|
|
273
273
|
"global instruction file",
|
|
274
274
|
],
|
|
275
|
+
// ⚠️ 上面的 resultTerms 是**一次历史事件的专名**(2026 年那轮三端连续性触发验证),
|
|
276
|
+
// 不是端清单 —— 端接入时不要往里加端名:resultTerms 是「谁被这个组认领」,
|
|
277
|
+
// 加了 "kimi" 等于断言"任何提到 kimi 的记忆都是那次三端验证的结论",
|
|
278
|
+
// 会让大批 kimi 记忆在无关任务下被这个组滤掉。同款判断见 932ec37:
|
|
279
|
+
// ROADMAP 里 `✅ managed continuity rules installed by setup` 那处也按"历史"原样保留。
|
|
280
|
+
//
|
|
281
|
+
// taskTerms 是「什么问法下这条历史仍然相关」,所以只补她真实的问法:
|
|
282
|
+
// 表里原有 "three-terminal" / "三终端",但她写的是"三端"/"四端"("三端"不是"三终端"的子串,
|
|
283
|
+
// 今天真的匹配不上)。**同样不补端名** —— 补了会让「kimi 挂了」这种无关任务
|
|
284
|
+
// 也解除过滤、把这条三端历史塞进 context;真正相关的问法已被 触发/接入/规则 覆盖。
|
|
275
285
|
taskTerms: [
|
|
276
286
|
"claude code",
|
|
277
287
|
"codex",
|
|
278
288
|
"gemini cli",
|
|
279
289
|
"three-terminal",
|
|
280
290
|
"三终端",
|
|
291
|
+
"三端",
|
|
292
|
+
"四端",
|
|
281
293
|
"trigger",
|
|
282
294
|
"触发",
|
|
283
295
|
"setup",
|
|
@@ -462,6 +474,18 @@ export const TASK_RESULT_SPECIFICITY_GROUPS = [
|
|
|
462
474
|
},
|
|
463
475
|
];
|
|
464
476
|
|
|
477
|
+
// 「某个端的验收 / 独立验证视角」这一簇。判据(context-composer-stable-selection.ts:154-157):
|
|
478
|
+
// resultTerms 命中记忆正文 && taskTerms 全不命中任务 → 丢弃这条记忆
|
|
479
|
+
// 两个字段方向相反,接入新端时**不要两边对称地加**:
|
|
480
|
+
// · taskTerms 加词 = 放宽豁免,召回变多 —— 端接入时要补的是这一侧
|
|
481
|
+
// · resultTerms 加词 = 扩大被滤对象,召回变少 —— 这里**有意不补端名**:
|
|
482
|
+
// 概念词(smoke / integration / 验收 / 验证视角 / 独立验证 / sidecar)本就是端无关的,
|
|
483
|
+
// 已经覆盖了"kimi 的验收视角"这类记忆;而裸词是子串匹配,补进去会把"顺带提了一句 kimi"
|
|
484
|
+
// 的无关偏好整条变成可滤对象,爆炸半径远大于收益。
|
|
485
|
+
// 实证:现有的裸词 "codex" 已经在误认领 —— 记忆 b8731a81(讲规则该放 home 级还是项目级)
|
|
486
|
+
// 只因正文里有 `.codex/AGENTS.md` 这个**路径片段**就被本组claim,
|
|
487
|
+
// 于是"四端的触发规则都装好了吗"这一问反而把最对题的那条滤掉了(shadow 实测)。
|
|
488
|
+
// 再往 resultTerms 加端名 = 把这个已知缺陷复制三份。
|
|
465
489
|
export const PREFERENCE_SPECIFICITY_GROUPS = [
|
|
466
490
|
{
|
|
467
491
|
resultTerms: [
|
|
@@ -480,6 +504,17 @@ export const PREFERENCE_SPECIFICITY_GROUPS = [
|
|
|
480
504
|
"claude code",
|
|
481
505
|
"codex",
|
|
482
506
|
"gemini cli",
|
|
507
|
+
// 在栈四端 = Claude Code / Codex / Kimi / AGY。AGY 是一个端,
|
|
508
|
+
// "antigravity" 与 "agy" 是它的两类写法(scope 前缀写 antigravity:,
|
|
509
|
+
// 她口头和 MEMORY.md 里写 agy),两个都要,不是两个端。
|
|
510
|
+
"kimi",
|
|
511
|
+
"antigravity",
|
|
512
|
+
"agy",
|
|
513
|
+
// 问端拓扑本身(不点名某一端)时也该解除:shadow 实测
|
|
514
|
+
// "四端的触发规则都装好了吗"会把 b8731a81(规则该放 home 级还是项目级,
|
|
515
|
+
// 正文写着"shared-behaviors.md 放三端通用底线")这条最对题的记忆滤掉。
|
|
516
|
+
"三端",
|
|
517
|
+
"四端",
|
|
483
518
|
"smoke",
|
|
484
519
|
"integration",
|
|
485
520
|
"验收",
|
package/src/ui-server.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { archiveDirtyBriefAsset, assetSummaryLine, buildBriefAsset, buildPinAsse
|
|
|
12
12
|
import { indexAsset, indexPinnedAsset } from "./asset-sync.js";
|
|
13
13
|
import { createComponentResolver, loadConfig, loadDotEnv } from "./runtime-config.js";
|
|
14
14
|
import { buildRetrievalContext, resolveScopeSelection } from "./scope-policy.js";
|
|
15
|
+
import { enforceLocalHttpRequestPolicy, LOCAL_HTTP_HOSTNAME } from "./http-request-policy.js";
|
|
15
16
|
import * as envConfig from "./env-config.js";
|
|
16
17
|
const config = (loadDotEnv(), loadConfig());
|
|
17
18
|
const getComponents = createComponentResolver(config);
|
|
@@ -188,8 +189,14 @@ async function handleSearch(mode: "search" | "explain" | "distill", body: Record
|
|
|
188
189
|
|
|
189
190
|
const server = Bun.serve({
|
|
190
191
|
port: readLimit(envConfig.uiPortRaw(), 4317, 1, 65535),
|
|
192
|
+
hostname: LOCAL_HTTP_HOSTNAME,
|
|
191
193
|
async fetch(request) {
|
|
192
194
|
try {
|
|
195
|
+
const policy = await enforceLocalHttpRequestPolicy(request);
|
|
196
|
+
if (!policy.allowed) {
|
|
197
|
+
return textResponse(policy.message, { status: policy.status });
|
|
198
|
+
}
|
|
199
|
+
|
|
193
200
|
const url = new URL(request.url);
|
|
194
201
|
|
|
195
202
|
if (request.method === "GET" && (url.pathname === "/" || url.pathname.startsWith("/ui/"))) {
|
package/src/version.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { metaDir } from "./compat.js";
|
|
5
|
+
|
|
6
|
+
type PackageMetadata = {
|
|
7
|
+
version?: unknown;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const packageMetadata = JSON.parse(
|
|
11
|
+
readFileSync(resolve(metaDir(import.meta), "../package.json"), "utf8"),
|
|
12
|
+
) as PackageMetadata;
|
|
13
|
+
|
|
14
|
+
if (typeof packageMetadata.version !== "string" || packageMetadata.version.length === 0) {
|
|
15
|
+
throw new Error("package.json must contain a non-empty version");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The public RecallNest version. package.json is the single source of truth. */
|
|
19
|
+
export const PACKAGE_VERSION = packageMetadata.version;
|