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/store.ts
CHANGED
|
@@ -5,11 +5,15 @@
|
|
|
5
5
|
import type * as LanceDB from "@lancedb/lancedb";
|
|
6
6
|
import { createHash, randomUUID } from "node:crypto";
|
|
7
7
|
import { existsSync, accessSync, constants, mkdirSync, realpathSync, lstatSync } from "node:fs";
|
|
8
|
-
import { dirname } from "node:path";
|
|
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";
|
|
14
|
+
import { withWriteLock } from "./distill-lock.js";
|
|
15
|
+
import { incrementWriteCount } from "./activity-counter.js";
|
|
16
|
+
import type { MemoryStorePort, MemoryStoreStats, MemoryStoreUpdate } from "./memory-store-port.js";
|
|
13
17
|
|
|
14
18
|
// ============================================================================
|
|
15
19
|
// Types
|
|
@@ -55,6 +59,13 @@ export interface MemorySearchResult {
|
|
|
55
59
|
export interface StoreConfig {
|
|
56
60
|
dbPath: string;
|
|
57
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;
|
|
58
69
|
}
|
|
59
70
|
|
|
60
71
|
export type LegacyScopeIssueKind = "missing" | "empty" | "global";
|
|
@@ -211,11 +222,16 @@ export function validateStoragePath(dbPath: string): string {
|
|
|
211
222
|
|
|
212
223
|
const TABLE_NAME = "memories";
|
|
213
224
|
|
|
214
|
-
export class MemoryStore {
|
|
225
|
+
export class MemoryStore implements MemoryStorePort {
|
|
215
226
|
private db: LanceDB.Connection | null = null;
|
|
216
227
|
private table: LanceDB.Table | null = null;
|
|
217
228
|
private initPromise: Promise<void> | null = null;
|
|
218
229
|
private ftsIndexCreated = false;
|
|
230
|
+
/** Per-id serial queues for patchMetadata; entries self-remove on settle. */
|
|
231
|
+
private readonly metadataPatchQueues = new Map<string, Promise<void>>();
|
|
232
|
+
|
|
233
|
+
/** Set when the init-time schema migration fails; surfaced in store error messages. */
|
|
234
|
+
private schemaMigrationError: string | null = null;
|
|
219
235
|
|
|
220
236
|
constructor(private readonly config: StoreConfig) {}
|
|
221
237
|
|
|
@@ -243,7 +259,13 @@ export class MemoryStore {
|
|
|
243
259
|
|
|
244
260
|
let db: LanceDB.Connection;
|
|
245
261
|
try {
|
|
246
|
-
|
|
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
|
+
});
|
|
247
269
|
} catch (err: any) {
|
|
248
270
|
const code = err.code || "";
|
|
249
271
|
const message = err.message || String(err);
|
|
@@ -261,24 +283,24 @@ export class MemoryStore {
|
|
|
261
283
|
try {
|
|
262
284
|
table = await db.openTable(TABLE_NAME);
|
|
263
285
|
|
|
264
|
-
// 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.
|
|
265
290
|
try {
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
275
|
-
if (!("fts_text" in sample[0])) {
|
|
276
|
-
logWarn("Adding fts_text column for backward compatibility");
|
|
277
|
-
await table.addColumns([{ name: "fts_text", valueSql: "text" }]);
|
|
278
|
-
}
|
|
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);
|
|
279
299
|
}
|
|
300
|
+
this.schemaMigrationError = null;
|
|
280
301
|
} catch (err) {
|
|
281
|
-
|
|
302
|
+
this.schemaMigrationError = err instanceof Error ? err.message : String(err);
|
|
303
|
+
logWarn("Could not check/migrate table schema — subsequent writes may fail:", err);
|
|
282
304
|
}
|
|
283
305
|
} catch (_openErr) {
|
|
284
306
|
// Table doesn't exist yet — create it
|
|
@@ -375,28 +397,33 @@ export class MemoryStore {
|
|
|
375
397
|
}
|
|
376
398
|
|
|
377
399
|
try {
|
|
378
|
-
|
|
400
|
+
// P0-2: go through upsert (delete+add under the global store-write lock) rather
|
|
401
|
+
// than a bare table.add — with the deterministic id above, storing the same
|
|
402
|
+
// (scope,text) twice now yields one row, not a dup-id pair (fixes the append bug).
|
|
403
|
+
return await this.upsert(fullEntry);
|
|
379
404
|
} catch (err: any) {
|
|
380
405
|
const code = err.code || "";
|
|
381
406
|
const message = err.message || String(err);
|
|
382
407
|
throw new Error(
|
|
383
|
-
`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}` : "")
|
|
384
410
|
);
|
|
385
411
|
}
|
|
386
|
-
return fullEntry;
|
|
387
412
|
}
|
|
388
413
|
|
|
389
414
|
/**
|
|
390
415
|
* Batch store multiple entries at once — much faster than individual store() calls.
|
|
391
416
|
* LanceDB handles bulk inserts efficiently with a single index update.
|
|
392
417
|
*/
|
|
393
|
-
async storeBatch(entries: Omit<MemoryEntry, "id" | "timestamp">[]): Promise<number> {
|
|
418
|
+
async storeBatch(entries: (Omit<MemoryEntry, "id" | "timestamp"> & { id?: string })[]): Promise<number> {
|
|
394
419
|
if (entries.length === 0) return 0;
|
|
395
420
|
await this.ensureInitialized();
|
|
396
421
|
|
|
397
422
|
const fullEntries: MemoryEntry[] = entries.map(entry => ({
|
|
398
423
|
...entry,
|
|
399
|
-
|
|
424
|
+
// P0-2: honor a caller-supplied id (consolidation insight/pattern idempotency),
|
|
425
|
+
// else the deterministic content id — unified with store() (was: always recompute).
|
|
426
|
+
id: entry.id ?? deterministicId(entry.scope, entry.text),
|
|
400
427
|
timestamp: Date.now(),
|
|
401
428
|
metadata: entry.metadata || "{}",
|
|
402
429
|
language: entry.language || "en",
|
|
@@ -413,16 +440,66 @@ export class MemoryStore {
|
|
|
413
440
|
return e;
|
|
414
441
|
});
|
|
415
442
|
|
|
443
|
+
// P2: collapse in-batch duplicate ids (latest-wins) BEFORE writing. Two entries
|
|
444
|
+
// with the same (scope,text) → same deterministic id; adding both would create a
|
|
445
|
+
// fresh dup-id pair in one shot (delete-set was deduped but the add was not).
|
|
446
|
+
const dedupedById = new Map<string, MemoryEntry>();
|
|
447
|
+
for (const e of enrichedEntries) dedupedById.set(e.id, e); // later occurrence wins
|
|
448
|
+
const toWrite = [...dedupedById.values()];
|
|
449
|
+
|
|
416
450
|
try {
|
|
417
|
-
await
|
|
451
|
+
await withWriteLock("store-write", async () => {
|
|
452
|
+
// P0-1/P0-2/P1: atomic idempotent batch write via mergeInsert (no delete+add
|
|
453
|
+
// window a crash could tear). toWrite is already in-batch deduped by id above,
|
|
454
|
+
// so the merge key matches ≤1 existing row (given no pre-existing dup-id rows).
|
|
455
|
+
await this.table!.mergeInsert("id").whenMatchedUpdateAll().whenNotMatchedInsertAll().execute(toWrite);
|
|
456
|
+
}, { expireMs: 30_000 });
|
|
418
457
|
} catch (err: any) {
|
|
419
458
|
const code = err.code || "";
|
|
420
459
|
const message = err.message || String(err);
|
|
421
460
|
throw new Error(
|
|
422
|
-
`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}` : "")
|
|
423
463
|
);
|
|
424
464
|
}
|
|
425
|
-
|
|
465
|
+
// Per-scope write counts drive dream activation. Best-effort — never block writes.
|
|
466
|
+
try {
|
|
467
|
+
const statsPath = this.activityStatsPath();
|
|
468
|
+
const countByScope = new Map<string, number>();
|
|
469
|
+
for (const e of toWrite) countByScope.set(e.scope, (countByScope.get(e.scope) ?? 0) + 1);
|
|
470
|
+
for (const [scope, n] of countByScope) incrementWriteCount(scope, n, { statsPath });
|
|
471
|
+
} catch { /* activity tracking is best-effort */ }
|
|
472
|
+
return toWrite.length;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* P0-1/P0-2/P1: Idempotent, crash-atomic write of a fully-formed entry, serialized
|
|
477
|
+
* across processes.
|
|
478
|
+
*
|
|
479
|
+
* Uses mergeInsert("id") (same primitive as update()) so the write is atomic — no
|
|
480
|
+
* delete+add window that a crash between the two steps could tear, losing the row
|
|
481
|
+
* (P1). The global `store-write` lock still serializes concurrent merges across the
|
|
482
|
+
* 11 mcp-server processes so two whenNotMatched inserts can't both fire for the same
|
|
483
|
+
* id (the P0-1 race). "Same id overwrites" gives content-addressed idempotency to
|
|
484
|
+
* every store()/storeBatch() caller whose id is deterministicId(scope,text).
|
|
485
|
+
* Precondition: no pre-existing dup-id rows in the table (run the dedup migration
|
|
486
|
+
* scripts/cleanup-duplicate-ids.ts first) — else the merge key match is ambiguous.
|
|
487
|
+
*/
|
|
488
|
+
/** Path to this store's activity-counter stats file, beside the LanceDB dir. */
|
|
489
|
+
private activityStatsPath(): string {
|
|
490
|
+
return join(dirname(this.config.dbPath), "activity-stats.json");
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
async upsert(entry: MemoryEntry): Promise<MemoryEntry> {
|
|
494
|
+
await this.ensureInitialized();
|
|
495
|
+
await withWriteLock("store-write", async () => {
|
|
496
|
+
await this.table!.mergeInsert("id").whenMatchedUpdateAll().whenNotMatchedInsertAll().execute([entry]);
|
|
497
|
+
}, { expireMs: 30_000 });
|
|
498
|
+
// Per-scope write count drives dream activation. Best-effort — never block a write.
|
|
499
|
+
try {
|
|
500
|
+
incrementWriteCount(entry.scope, 1, { statsPath: this.activityStatsPath() });
|
|
501
|
+
} catch { /* activity tracking is best-effort */ }
|
|
502
|
+
return entry;
|
|
426
503
|
}
|
|
427
504
|
|
|
428
505
|
/**
|
|
@@ -617,11 +694,19 @@ export class MemoryStore {
|
|
|
617
694
|
if (isFullId) {
|
|
618
695
|
candidates = await this.table!.query().where(`id = '${id}'`).limit(1).toArray();
|
|
619
696
|
} else {
|
|
620
|
-
//
|
|
621
|
-
|
|
622
|
-
|
|
697
|
+
// v2.5.2 (2026-05-27): Use SQL LIKE for prefix lookup instead of app-layer scan
|
|
698
|
+
// with limit(1000) — the old `.select().limit(1000).filter()` pattern silently
|
|
699
|
+
// missed entries beyond row 1000 in 90K+ libraries (Codex trio review finding,
|
|
700
|
+
// ref: ~/Desktop/codex-v2.5.1-fix-review-20260527.md, store.delete limit 漏洞).
|
|
701
|
+
// Now mirrors getById prefix behavior: LIKE with limit(2) detects ambiguity.
|
|
702
|
+
const safeId = escapeSqlLiteral(id);
|
|
703
|
+
candidates = await this.table!.query()
|
|
704
|
+
.select(["id", "scope"])
|
|
705
|
+
.where(`id LIKE '${safeId}%'`)
|
|
706
|
+
.limit(2)
|
|
707
|
+
.toArray();
|
|
623
708
|
if (candidates.length > 1) {
|
|
624
|
-
throw new Error(`Ambiguous prefix "${id}" matches ${candidates.length} memories. Use a longer prefix or full ID.`);
|
|
709
|
+
throw new Error(`Ambiguous prefix "${id}" matches ${candidates.length}+ memories. Use a longer prefix or full ID.`);
|
|
625
710
|
}
|
|
626
711
|
}
|
|
627
712
|
if (candidates.length === 0) {
|
|
@@ -688,6 +773,72 @@ export class MemoryStore {
|
|
|
688
773
|
.slice(offset, offset + limit);
|
|
689
774
|
}
|
|
690
775
|
|
|
776
|
+
/**
|
|
777
|
+
* True DB-level pagination — where/limit/offset are pushed down to LanceDB,
|
|
778
|
+
* so unlike list() this never materializes the full match set in JS. Use for
|
|
779
|
+
* full-corpus maintenance sweeps (auto-gc, promotion scan, lint/checkup).
|
|
780
|
+
*
|
|
781
|
+
* Caveats:
|
|
782
|
+
* - No orderBy support in the LanceDB query API: rows come back in fragment
|
|
783
|
+
* scan order, NOT timestamp order. Stable across pages for a read-only
|
|
784
|
+
* sweep, but concurrent writes rewrite fragments and may shift rows
|
|
785
|
+
* between pages (a missed row is picked up by the next sweep). Do not use
|
|
786
|
+
* for user-facing ordered listings — that's what list() is for.
|
|
787
|
+
* - includeVector loads the embedding column; only request it when the
|
|
788
|
+
* consumer actually needs vectors (they dominate row size).
|
|
789
|
+
*/
|
|
790
|
+
async listPage(opts: {
|
|
791
|
+
scopeFilter?: string[];
|
|
792
|
+
category?: string;
|
|
793
|
+
limit?: number;
|
|
794
|
+
offset?: number;
|
|
795
|
+
includeVector?: boolean;
|
|
796
|
+
} = {}): Promise<MemoryEntry[]> {
|
|
797
|
+
await this.ensureInitialized();
|
|
798
|
+
const { scopeFilter, category, limit = 1000, offset = 0, includeVector = false } = opts;
|
|
799
|
+
|
|
800
|
+
let query = this.table!.query();
|
|
801
|
+
|
|
802
|
+
const conditions: string[] = [];
|
|
803
|
+
if (scopeFilter && scopeFilter.length > 0) {
|
|
804
|
+
const scopeConditions = scopeFilter
|
|
805
|
+
.map(scope => {
|
|
806
|
+
const safe = escapeSqlLiteral(scope);
|
|
807
|
+
return scope.includes(":")
|
|
808
|
+
? `scope = '${safe}'`
|
|
809
|
+
: `scope LIKE '${safe}%'`;
|
|
810
|
+
})
|
|
811
|
+
.join(" OR ");
|
|
812
|
+
conditions.push(`((${scopeConditions}))`);
|
|
813
|
+
}
|
|
814
|
+
if (category) {
|
|
815
|
+
conditions.push(`category = '${escapeSqlLiteral(category)}'`);
|
|
816
|
+
}
|
|
817
|
+
if (conditions.length > 0) {
|
|
818
|
+
query = query.where(conditions.join(" AND "));
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const columns = ["id", "text", "category", "scope", "importance", "timestamp", "metadata"];
|
|
822
|
+
if (includeVector) columns.push("vector");
|
|
823
|
+
|
|
824
|
+
const results = await query
|
|
825
|
+
.select(columns)
|
|
826
|
+
.limit(limit)
|
|
827
|
+
.offset(offset)
|
|
828
|
+
.toArray();
|
|
829
|
+
|
|
830
|
+
return results.map((row): MemoryEntry => ({
|
|
831
|
+
id: row.id as string,
|
|
832
|
+
text: row.text as string,
|
|
833
|
+
vector: includeVector ? Array.from(row.vector as Iterable<number>) : [],
|
|
834
|
+
category: row.category as MemoryEntry["category"],
|
|
835
|
+
scope: (row.scope as string | undefined) ?? "",
|
|
836
|
+
importance: Number(row.importance),
|
|
837
|
+
timestamp: Number(row.timestamp),
|
|
838
|
+
metadata: (row.metadata as string) || "{}",
|
|
839
|
+
}));
|
|
840
|
+
}
|
|
841
|
+
|
|
691
842
|
async get(id: string, scopeFilter?: string[]): Promise<MemoryEntry | null> {
|
|
692
843
|
await this.ensureInitialized();
|
|
693
844
|
|
|
@@ -741,18 +892,35 @@ export class MemoryStore {
|
|
|
741
892
|
}
|
|
742
893
|
|
|
743
894
|
/**
|
|
744
|
-
* Get a single entry by
|
|
745
|
-
* Lightweight read used by AccessTracker for metadata updates
|
|
895
|
+
* Get a single entry by ID (full UUID) or 8+ hex prefix.
|
|
896
|
+
* Lightweight read used by AccessTracker for metadata updates and by skill-engine
|
|
897
|
+
* for outcome recording.
|
|
898
|
+
*
|
|
899
|
+
* v2.5.1 (2026-05-27) — 加 prefix lookup support:和 update() / delete() 行为对齐,
|
|
900
|
+
* 让 agent 拿到 short id(如 store_skill 返回的 8 位前缀、人类粘贴的截断 id)也能
|
|
901
|
+
* resolve 到完整 entry。歧义 prefix(匹配 >1 条)返回 null 让 caller 处理。
|
|
902
|
+
* 现有 caller 全部传完整 UUID,向后兼容。
|
|
746
903
|
*/
|
|
747
904
|
async getById(id: string): Promise<MemoryEntry | null> {
|
|
748
905
|
await this.ensureInitialized();
|
|
906
|
+
|
|
907
|
+
// Support both full UUID and 8+ hex prefix (same pattern as update() / delete()).
|
|
908
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
909
|
+
const prefixRegex = /^[0-9a-f]{8,}$/i;
|
|
910
|
+
const isFullId = uuidRegex.test(id);
|
|
911
|
+
const isPrefix = !isFullId && prefixRegex.test(id);
|
|
912
|
+
if (!isFullId && !isPrefix) return null;
|
|
913
|
+
|
|
749
914
|
const safeId = escapeSqlLiteral(id);
|
|
915
|
+
const whereClause = isFullId ? `id = '${safeId}'` : `id LIKE '${safeId}%'`;
|
|
916
|
+
|
|
750
917
|
const rows = await this.table!.query()
|
|
751
918
|
.select(["id", "text", "vector", "category", "scope", "importance", "timestamp", "metadata"])
|
|
752
|
-
.where(
|
|
753
|
-
.limit(1)
|
|
919
|
+
.where(whereClause)
|
|
920
|
+
.limit(2) // detect ambiguous prefix (matches >1)
|
|
754
921
|
.toArray();
|
|
755
922
|
if (rows.length === 0) return null;
|
|
923
|
+
if (isPrefix && rows.length > 1) return null; // ambiguous prefix
|
|
756
924
|
const row = rows[0];
|
|
757
925
|
return {
|
|
758
926
|
id: row.id as string,
|
|
@@ -773,28 +941,28 @@ export class MemoryStore {
|
|
|
773
941
|
*/
|
|
774
942
|
async getVectors(ids: string[]): Promise<Map<string, number[]>> {
|
|
775
943
|
await this.ensureInitialized();
|
|
776
|
-
if (ids.length === 0) return new Map();
|
|
777
|
-
|
|
778
|
-
const conditions = ids.map(id => `id = '${escapeSqlLiteral(id)}'`).join(" OR ");
|
|
779
|
-
const rows = await this.table!.query()
|
|
780
|
-
.select(["id", "vector"])
|
|
781
|
-
.where(conditions)
|
|
782
|
-
.limit(ids.length)
|
|
783
|
-
.toArray();
|
|
784
|
-
|
|
785
944
|
const result = new Map<string, number[]>();
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
945
|
+
if (ids.length === 0) return result;
|
|
946
|
+
|
|
947
|
+
// 分批:避免 id 很多时 OR 链过长(如 data-checkup/memory-lint 一次补几千条向量)。
|
|
948
|
+
const BATCH = 500;
|
|
949
|
+
for (let i = 0; i < ids.length; i += BATCH) {
|
|
950
|
+
const batch = ids.slice(i, i + BATCH);
|
|
951
|
+
const conditions = batch.map(id => `id = '${escapeSqlLiteral(id)}'`).join(" OR ");
|
|
952
|
+
const rows = await this.table!.query()
|
|
953
|
+
.select(["id", "vector"])
|
|
954
|
+
.where(conditions)
|
|
955
|
+
.limit(batch.length)
|
|
956
|
+
.toArray();
|
|
957
|
+
for (const row of rows) {
|
|
958
|
+
const vec = Array.from(row.vector as Iterable<number>);
|
|
959
|
+
if (vec.length > 0) result.set(row.id as string, vec);
|
|
960
|
+
}
|
|
789
961
|
}
|
|
790
962
|
return result;
|
|
791
963
|
}
|
|
792
964
|
|
|
793
|
-
async stats(scopeFilter?: string[]): Promise<{
|
|
794
|
-
totalCount: number;
|
|
795
|
-
scopeCounts: Record<string, number>;
|
|
796
|
-
categoryCounts: Record<string, number>
|
|
797
|
-
}> {
|
|
965
|
+
async stats(scopeFilter?: string[]): Promise<MemoryStoreStats> {
|
|
798
966
|
await this.ensureInitialized();
|
|
799
967
|
|
|
800
968
|
let query = this.table!.query();
|
|
@@ -874,7 +1042,7 @@ export class MemoryStore {
|
|
|
874
1042
|
|
|
875
1043
|
async update(
|
|
876
1044
|
id: string,
|
|
877
|
-
updates:
|
|
1045
|
+
updates: MemoryStoreUpdate,
|
|
878
1046
|
scopeFilter?: string[]
|
|
879
1047
|
): Promise<MemoryEntry | null> {
|
|
880
1048
|
await this.ensureInitialized();
|
|
@@ -916,7 +1084,10 @@ export class MemoryStore {
|
|
|
916
1084
|
const updated: MemoryEntry = {
|
|
917
1085
|
id: row.id as string,
|
|
918
1086
|
text: updates.text ?? (row.text as string),
|
|
919
|
-
|
|
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>) : []),
|
|
920
1091
|
category: updates.category ?? (row.category as MemoryEntry["category"]),
|
|
921
1092
|
scope: rowScope,
|
|
922
1093
|
importance: updates.importance ?? Number(row.importance),
|
|
@@ -936,14 +1107,94 @@ export class MemoryStore {
|
|
|
936
1107
|
}
|
|
937
1108
|
}
|
|
938
1109
|
|
|
939
|
-
//
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
await this.table
|
|
1110
|
+
// Atomic upsert via mergeInsert — replaces the previous delete+add two-step,
|
|
1111
|
+
// which could lose the row entirely on a crash between steps and exposed a
|
|
1112
|
+
// window where concurrent reads saw the id momentarily vanish.
|
|
1113
|
+
await this.table!
|
|
1114
|
+
.mergeInsert("id")
|
|
1115
|
+
.whenMatchedUpdateAll()
|
|
1116
|
+
.whenNotMatchedInsertAll()
|
|
1117
|
+
.execute([updated]);
|
|
943
1118
|
|
|
944
1119
|
return updated;
|
|
945
1120
|
}
|
|
946
1121
|
|
|
1122
|
+
/**
|
|
1123
|
+
* Serialized read-modify-write for the metadata column.
|
|
1124
|
+
*
|
|
1125
|
+
* Every direct `getById → mutate metadata → update` round-trip can lose
|
|
1126
|
+
* concurrent increments (last write wins). This method funnels metadata
|
|
1127
|
+
* patches for the same id through a per-id promise queue so each patch
|
|
1128
|
+
* reads the result of the previous one.
|
|
1129
|
+
*
|
|
1130
|
+
* patchFn receives the parsed metadata object plus the freshly-read entry
|
|
1131
|
+
* (for callers that need importance/timestamp context) and returns the new
|
|
1132
|
+
* metadata object. A throwing patchFn abandons that patch — the error
|
|
1133
|
+
* propagates to the caller, later queued patches still run.
|
|
1134
|
+
*
|
|
1135
|
+
* Scope of guarantee: in-process only. Cross-process writers (multiple
|
|
1136
|
+
* mcp-server instances) and the remaining direct update() callers
|
|
1137
|
+
* (consolidation/capture/forget/prospective/conflict engines, auto-gc)
|
|
1138
|
+
* are NOT serialized by this queue. Long-term direction: make this the
|
|
1139
|
+
* only legal metadata write path.
|
|
1140
|
+
*/
|
|
1141
|
+
async patchMetadata(
|
|
1142
|
+
id: string,
|
|
1143
|
+
patchFn: (meta: Record<string, unknown>, entry: MemoryEntry) => Record<string, unknown>,
|
|
1144
|
+
scopeFilter?: string[],
|
|
1145
|
+
): Promise<MemoryEntry | null> {
|
|
1146
|
+
await this.ensureInitialized();
|
|
1147
|
+
|
|
1148
|
+
// Queue key must be the resolved full UUID — otherwise a prefix caller and
|
|
1149
|
+
// a full-id caller targeting the same row would race on separate queues.
|
|
1150
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1151
|
+
let fullId: string;
|
|
1152
|
+
if (uuidRegex.test(id)) {
|
|
1153
|
+
fullId = id;
|
|
1154
|
+
} else {
|
|
1155
|
+
const resolved = await this.getById(id);
|
|
1156
|
+
if (!resolved) return null;
|
|
1157
|
+
fullId = resolved.id;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
const run = async (): Promise<MemoryEntry | null> => {
|
|
1161
|
+
const entry = await this.getById(fullId);
|
|
1162
|
+
if (!entry) return null;
|
|
1163
|
+
let meta: Record<string, unknown>;
|
|
1164
|
+
try {
|
|
1165
|
+
const parsed: unknown = JSON.parse(entry.metadata || "{}");
|
|
1166
|
+
meta = parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
|
|
1167
|
+
? (parsed as Record<string, unknown>)
|
|
1168
|
+
: {};
|
|
1169
|
+
} catch {
|
|
1170
|
+
meta = {};
|
|
1171
|
+
}
|
|
1172
|
+
const patched = patchFn(meta, entry);
|
|
1173
|
+
return this.update(fullId, { metadata: JSON.stringify(patched) }, scopeFilter);
|
|
1174
|
+
};
|
|
1175
|
+
|
|
1176
|
+
const prev = this.metadataPatchQueues.get(fullId) ?? Promise.resolve();
|
|
1177
|
+
// Isolate prior failures so one rejected patch can't wedge the queue.
|
|
1178
|
+
const result = prev.then(run, run);
|
|
1179
|
+
// The stored tail settles regardless of outcome; the caller keeps the real
|
|
1180
|
+
// rejection via `result`. Only the current tail removes itself.
|
|
1181
|
+
const tail = result.then(
|
|
1182
|
+
() => undefined,
|
|
1183
|
+
() => undefined,
|
|
1184
|
+
).then(() => {
|
|
1185
|
+
if (this.metadataPatchQueues.get(fullId) === tail) {
|
|
1186
|
+
this.metadataPatchQueues.delete(fullId);
|
|
1187
|
+
}
|
|
1188
|
+
});
|
|
1189
|
+
this.metadataPatchQueues.set(fullId, tail);
|
|
1190
|
+
return result;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
/** Number of ids with in-flight metadata patches (test/diagnostic hook). */
|
|
1194
|
+
get pendingMetadataPatchCount(): number {
|
|
1195
|
+
return this.metadataPatchQueues.size;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
947
1198
|
async bulkDelete(scopeFilter: string[], beforeTimestamp?: number): Promise<number> {
|
|
948
1199
|
await this.ensureInitialized();
|
|
949
1200
|
|
|
@@ -983,6 +1234,19 @@ export class MemoryStore {
|
|
|
983
1234
|
return deleteCount;
|
|
984
1235
|
}
|
|
985
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
|
+
|
|
986
1250
|
get hasFtsSupport(): boolean {
|
|
987
1251
|
return this.ftsIndexCreated;
|
|
988
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
|
"验收",
|
|
@@ -59,7 +59,9 @@ const RULES: CompressionRule[] = [
|
|
|
59
59
|
|
|
60
60
|
// Patterns that identify tool output blocks in conversation text.
|
|
61
61
|
// Claude Code / Codex format: command on one line, output follows.
|
|
62
|
-
|
|
62
|
+
// 末项用 (?![\s\S]) 表示「字符串绝对结尾」。不能用 \z(JS 无此锚点,会被当字面字符 z →
|
|
63
|
+
// 在输出里第一个 z 处误截断),也不能用 $(gm 模式下匹配每行尾,惰性 [\s\S]*? 会在第一行就停)。
|
|
64
|
+
const TOOL_OUTPUT_BLOCK = /^(?:\$\s+|❯\s+|>\s+)(.+)\n([\s\S]*?)(?=\n(?:\$\s+|❯\s+|>\s+)|\n\n[A-Z]|\n##|(?![\s\S]))/gm;
|
|
63
65
|
|
|
64
66
|
// Base64 image data (screenshots, etc.)
|
|
65
67
|
const BASE64_BLOCK = /(?:data:image\/[^;]+;base64,)?[A-Za-z0-9+/]{500,}={0,2}/g;
|
package/src/ui-server.ts
CHANGED
|
@@ -12,6 +12,8 @@ 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";
|
|
16
|
+
import * as envConfig from "./env-config.js";
|
|
15
17
|
const config = (loadDotEnv(), loadConfig());
|
|
16
18
|
const getComponents = createComponentResolver(config);
|
|
17
19
|
|
|
@@ -186,9 +188,15 @@ async function handleSearch(mode: "search" | "explain" | "distill", body: Record
|
|
|
186
188
|
}
|
|
187
189
|
|
|
188
190
|
const server = Bun.serve({
|
|
189
|
-
port: readLimit(
|
|
191
|
+
port: readLimit(envConfig.uiPortRaw(), 4317, 1, 65535),
|
|
192
|
+
hostname: LOCAL_HTTP_HOSTNAME,
|
|
190
193
|
async fetch(request) {
|
|
191
194
|
try {
|
|
195
|
+
const policy = await enforceLocalHttpRequestPolicy(request);
|
|
196
|
+
if (!policy.allowed) {
|
|
197
|
+
return textResponse(policy.message, { status: policy.status });
|
|
198
|
+
}
|
|
199
|
+
|
|
192
200
|
const url = new URL(request.url);
|
|
193
201
|
|
|
194
202
|
if (request.method === "GET" && (url.pathname === "/" || url.pathname.startsWith("/ui/"))) {
|