recallnest 3.0.0 → 4.0.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 +304 -0
- package/README.md +251 -258
- package/README_CN.md +226 -247
- package/assets/ui/app.js +6 -1
- package/bin/recallnest.mjs +20 -37
- package/docs/api-reference.md +8 -0
- package/docs/architecture.md +18 -13
- package/eval/cases-canary.json +107 -9
- package/integrations/examples/claude-agent-sdk/memory-agent.ts +2 -0
- package/integrations/examples/langchain/memory-chain.py +4 -0
- package/integrations/examples/openai-agents-sdk/memory-agent.py +4 -0
- package/package.json +12 -3
- package/src/activity-counter.ts +1 -0
- package/src/adaptive-retrieval.ts +23 -0
- package/src/api-server.ts +13 -11
- package/src/asset-sync.ts +26 -2
- package/src/audit-log.ts +2 -0
- package/src/auto-gc.ts +4 -3
- package/src/belief-history.ts +137 -31
- package/src/capture-engine.ts +459 -71
- package/src/cascade-forget.ts +7 -3
- package/src/checkpoint-mirror.ts +417 -0
- package/src/cli.ts +574 -54
- package/src/conflict-engine.ts +43 -15
- package/src/consolidation-engine.ts +24 -3
- package/src/context-collapse-renderer.ts +8 -1
- package/src/context-composer-stable.ts +5 -1
- package/src/context-composer.ts +11 -2
- package/src/distill-lock.ts +246 -74
- package/src/dream-pipeline.ts +3 -1
- package/src/embedder.ts +24 -0
- package/src/env-config.ts +136 -0
- package/src/flock.ts +97 -0
- package/src/forget-engine.ts +342 -58
- package/src/gateway-server.ts +4 -0
- package/src/ingest-plan.ts +56 -0
- package/src/ingest.ts +294 -121
- package/src/kg-contradiction.ts +753 -0
- package/src/kg-extractor.ts +17 -0
- package/src/kg-store.ts +1 -1
- package/src/known-item-eval.ts +455 -0
- package/src/llm-client.ts +13 -26
- package/src/llm-consolidation.ts +2 -2
- package/src/mcp-tool-deps.ts +6 -0
- package/src/mcp-tools-advanced.ts +80 -45
- package/src/mcp-tools-core.ts +23 -17
- package/src/mcp-tools-governance.ts +13 -7
- package/src/memory-assets.ts +144 -8
- package/src/memory-boundaries.ts +19 -0
- package/src/memory-evolution.ts +9 -0
- package/src/memory-lint.ts +1 -1
- package/src/memory-output.ts +311 -14
- package/src/memory-reconcile.ts +1437 -0
- package/src/memory-schema.ts +11 -0
- package/src/memory-store-port.ts +1 -0
- package/src/minis-archive.ts +151 -0
- package/src/noise-filter.ts +15 -2
- package/src/pii-detector.ts +31 -1
- package/src/pivot-apply.ts +55 -5
- package/src/pivot-evidence.ts +128 -0
- package/src/ppr-traversal.ts +25 -1
- package/src/preference-matcher.ts +40 -28
- package/src/prospective-memory.ts +7 -4
- package/src/retriever.ts +297 -57
- package/src/retroactive-boost.ts +3 -2
- package/src/runtime-config.ts +15 -1
- package/src/scope-policy.ts +1 -0
- package/src/session-distiller.ts +6 -2
- package/src/session-engine.ts +11 -4
- package/src/session-output.ts +20 -1
- package/src/session-schema.ts +5 -2
- package/src/session-store.ts +59 -12
- package/src/skill-promotion.ts +67 -1
- package/src/store-receipts.ts +109 -0
- package/src/store-write-budget.ts +37 -0
- package/src/store.ts +298 -57
- package/src/text-fingerprint.ts +25 -0
- package/src/trigger-store.ts +443 -0
- package/src/ui-server.ts +22 -21
- package/src/version-manager.ts +19 -5
- package/src/write-verifier.ts +16 -11
- package/src/__tests__/access-tracker-novelty.test.ts +0 -324
- package/src/__tests__/activity-counter.test.ts +0 -125
- package/src/__tests__/adaptive-fidelity.test.ts +0 -102
- package/src/__tests__/admission-control.test.ts +0 -206
- package/src/__tests__/age-label.test.ts +0 -33
- package/src/__tests__/anchor-generator.test.ts +0 -67
- package/src/__tests__/api-server-security.test.ts +0 -102
- package/src/__tests__/auto-consolidation.test.ts +0 -168
- package/src/__tests__/auto-recall.test.ts +0 -131
- package/src/__tests__/babel-integration.test.ts +0 -144
- package/src/__tests__/batch-dedup.test.ts +0 -65
- package/src/__tests__/belief-history.test.ts +0 -220
- package/src/__tests__/canary-eval.test.ts +0 -114
- package/src/__tests__/capture-engine.test.ts +0 -1328
- package/src/__tests__/capture-heuristic.test.ts +0 -171
- package/src/__tests__/cascade-forget.test.ts +0 -210
- package/src/__tests__/category-min-scores.test.ts +0 -116
- package/src/__tests__/cc8-cc1-essential-context.test.ts +0 -298
- package/src/__tests__/circuit-breaker.test.ts +0 -99
- package/src/__tests__/cluster-consolidation.test.ts +0 -1236
- package/src/__tests__/confidence-metadata.test.ts +0 -125
- package/src/__tests__/confidence-tracker.test.ts +0 -261
- package/src/__tests__/conflict-advisor.test.ts +0 -206
- package/src/__tests__/conflict-escalation.test.ts +0 -149
- package/src/__tests__/conflict-lifecycle.test.ts +0 -83
- package/src/__tests__/conflict-output.test.ts +0 -85
- package/src/__tests__/conflict-store.test.ts +0 -418
- package/src/__tests__/connector-v1.test.ts +0 -181
- package/src/__tests__/consolidation-engine.test.ts +0 -520
- package/src/__tests__/constructive-retrieval.test.ts +0 -509
- package/src/__tests__/context-collapse-renderer.test.ts +0 -214
- package/src/__tests__/context-composer-pins.test.ts +0 -64
- package/src/__tests__/context-composer-task-selection.test.ts +0 -24
- package/src/__tests__/context-reconstructor.test.ts +0 -65
- package/src/__tests__/context-renderer.test.ts +0 -91
- package/src/__tests__/continuity-eval.test.ts +0 -275
- package/src/__tests__/continuity-schema.test.ts +0 -97
- package/src/__tests__/contracts.test.ts +0 -262
- package/src/__tests__/conversation-importer.test.ts +0 -692
- package/src/__tests__/core-recall-eval.test.ts +0 -235
- package/src/__tests__/core-summary.test.ts +0 -109
- package/src/__tests__/data-checkup.test.ts +0 -304
- package/src/__tests__/decay-exemption.test.ts +0 -129
- package/src/__tests__/dedup-actions.test.ts +0 -58
- package/src/__tests__/dedup-guard.test.ts +0 -39
- package/src/__tests__/deterministic-id.test.ts +0 -39
- package/src/__tests__/distill-lock.test.ts +0 -305
- package/src/__tests__/distill-session-argshape.test.ts +0 -79
- package/src/__tests__/doctor.test.ts +0 -319
- package/src/__tests__/dream-pipeline.test.ts +0 -820
- package/src/__tests__/dream-scope-isolation.test.ts +0 -386
- package/src/__tests__/dream-write-batching.test.ts +0 -211
- package/src/__tests__/drill-down.test.ts +0 -108
- package/src/__tests__/embedder.test.ts +0 -122
- package/src/__tests__/emotion-decay.test.ts +0 -46
- package/src/__tests__/emotion-detector.test.ts +0 -56
- package/src/__tests__/emotion-phase1-integration.test.ts +0 -153
- package/src/__tests__/emotion-retrieval.test.ts +0 -56
- package/src/__tests__/entity-resolver.test.ts +0 -58
- package/src/__tests__/env-config-invariant.test.ts +0 -80
- package/src/__tests__/env-config.test.ts +0 -211
- package/src/__tests__/error-signature-boost.test.ts +0 -87
- package/src/__tests__/error-signature.test.ts +0 -95
- package/src/__tests__/error-taxonomy.test.ts +0 -129
- package/src/__tests__/ethics-layer.test.ts +0 -476
- package/src/__tests__/event-segmenter.test.ts +0 -153
- package/src/__tests__/feature-flags-integration.test.ts +0 -54
- package/src/__tests__/frequency-tracker.test.ts +0 -113
- package/src/__tests__/freshness.test.ts +0 -186
- package/src/__tests__/graph-export.test.ts +0 -489
- package/src/__tests__/hotness-blend.test.ts +0 -283
- package/src/__tests__/ingest-chunking.test.ts +0 -44
- package/src/__tests__/ingest-scope-prefix.test.ts +0 -69
- package/src/__tests__/ingest.test.ts +0 -360
- package/src/__tests__/interference-detector.test.ts +0 -203
- package/src/__tests__/kg-extractor.test.ts +0 -192
- package/src/__tests__/kg-store.test.ts +0 -300
- package/src/__tests__/llm-client-pivot.test.ts +0 -628
- package/src/__tests__/llm-consolidation.test.ts +0 -226
- package/src/__tests__/maintenance-locks.test.ts +0 -197
- package/src/__tests__/mcp-efficiency.test.ts +0 -288
- package/src/__tests__/mcp-related-scopes.test.ts +0 -151
- package/src/__tests__/memory-boundaries.test.ts +0 -282
- package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +0 -362
- package/src/__tests__/memory-evolution.test.ts +0 -271
- package/src/__tests__/memory-health-rebalance.test.ts +0 -144
- package/src/__tests__/memory-lint.test.ts +0 -421
- package/src/__tests__/memory-output.test.ts +0 -224
- package/src/__tests__/memory-promotion.test.ts +0 -315
- package/src/__tests__/memory-utility.test.ts +0 -256
- package/src/__tests__/migrate-dedup-ids.test.ts +0 -65
- package/src/__tests__/multi-vector.test.ts +0 -242
- package/src/__tests__/narrative-architecture.test.ts +0 -456
- package/src/__tests__/noise-filter.test.ts +0 -205
- package/src/__tests__/noise-prototype-bank.test.ts +0 -280
- package/src/__tests__/obsidian-connector.test.ts +0 -363
- package/src/__tests__/omitted-reporting.test.ts +0 -98
- package/src/__tests__/openai-contract.test.ts +0 -513
- package/src/__tests__/pending-review.test.ts +0 -77
- package/src/__tests__/pii-redact.test.ts +0 -333
- package/src/__tests__/pivot-apply.test.ts +0 -373
- package/src/__tests__/pivot-distill-supervisor.test.ts +0 -457
- package/src/__tests__/pivot-distill.test.ts +0 -1178
- package/src/__tests__/post-retrieval-filter.test.ts +0 -178
- package/src/__tests__/ppr-traversal.test.ts +0 -256
- package/src/__tests__/prediction-engine.test.ts +0 -349
- package/src/__tests__/predictive-prospective-memory.test.ts +0 -416
- package/src/__tests__/preference-guard-smoke.test.ts +0 -211
- package/src/__tests__/preference-matcher.test.ts +0 -282
- package/src/__tests__/preference-slots.test.ts +0 -205
- package/src/__tests__/promotion-distinct-sources.test.ts +0 -162
- package/src/__tests__/prospective-memory.test.ts +0 -283
- package/src/__tests__/pull-from-macbook-sync-only.test.ts +0 -679
- package/src/__tests__/query-entity-detector.test.ts +0 -171
- package/src/__tests__/query-expander-alias.test.ts +0 -146
- package/src/__tests__/query-expander.test.ts +0 -34
- package/src/__tests__/recall-gate.test.ts +0 -266
- package/src/__tests__/recall-governor.test.ts +0 -227
- package/src/__tests__/recall-mode.test.ts +0 -64
- package/src/__tests__/resolve-db-path.test.ts +0 -130
- package/src/__tests__/resolve-tier.test.ts +0 -64
- package/src/__tests__/result-synthesizer.test.ts +0 -198
- package/src/__tests__/resume-context.test.ts +0 -4898
- package/src/__tests__/retrieval-eval.test.ts +0 -163
- package/src/__tests__/retrieval-trace.test.ts +0 -213
- package/src/__tests__/retrieve-audit-revision.test.ts +0 -255
- package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
- package/src/__tests__/retriever-lazy-init-dispatch.test.ts +0 -123
- package/src/__tests__/retroactive-boost.test.ts +0 -223
- package/src/__tests__/salience-scoring.test.ts +0 -106
- package/src/__tests__/scope-inventory.test.ts +0 -200
- package/src/__tests__/scope-policy.test.ts +0 -69
- package/src/__tests__/scope-suggester.test.ts +0 -80
- package/src/__tests__/session-checkpoint.test.ts +0 -291
- package/src/__tests__/session-distiller.test.ts +0 -640
- package/src/__tests__/session-gc.test.ts +0 -215
- package/src/__tests__/skill-engine.test.ts +0 -552
- package/src/__tests__/skill-promotion.test.ts +0 -654
- package/src/__tests__/skill-verifier.test.ts +0 -122
- package/src/__tests__/source-heartbeat.test.ts +0 -254
- package/src/__tests__/stable-context-length-contract.test.ts +0 -62
- package/src/__tests__/store-patch-metadata.test.ts +0 -235
- package/src/__tests__/store-read-consistency.test.ts +0 -122
- package/src/__tests__/store-schema-migration.test.ts +0 -87
- package/src/__tests__/store-scope-audit.test.ts +0 -81
- package/src/__tests__/store-upsert.test.ts +0 -201
- package/src/__tests__/supersede-chain.test.ts +0 -154
- package/src/__tests__/synthesis-contract.test.ts +0 -252
- package/src/__tests__/synthesis-promotion.test.ts +0 -369
- package/src/__tests__/temporal-parser.test.ts +0 -241
- package/src/__tests__/temporal-validity.test.ts +0 -160
- package/src/__tests__/term-registry-terminal-roster.test.ts +0 -127
- package/src/__tests__/term-registry.test.ts +0 -121
- package/src/__tests__/tool-error-surfacing.test.ts +0 -98
- package/src/__tests__/tool-output-compressor.test.ts +0 -160
- package/src/__tests__/topic-tag.test.ts +0 -113
- package/src/__tests__/trusted-memory.test.ts +0 -867
- package/src/__tests__/ui-server-security.test.ts +0 -93
- package/src/__tests__/usage-tracker.test.ts +0 -287
- package/src/__tests__/utility-weight.test.ts +0 -57
- package/src/__tests__/verbatim-self-recall.test.ts +0 -279
- package/src/__tests__/version-manager.test.ts +0 -248
- package/src/__tests__/workflow-observation-skill-outcome.test.ts +0 -352
- package/src/__tests__/workflow-observation.test.ts +0 -433
- package/src/__tests__/write-verifier.test.ts +0 -122
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Verbatim self-recall: can a memory be found using words copied out of its own body?
|
|
3
|
-
*
|
|
4
|
-
* ## Where this came from
|
|
5
|
-
*
|
|
6
|
-
* On 2026-08-13 a window tried to correct a stored case (`4cb4853e`) and could not reach
|
|
7
|
-
* it. `explain_memory` was called twice, eleven recall slots in total, with queries taken
|
|
8
|
-
* verbatim from that case's own text. The target came back zero times. Ranking first in
|
|
9
|
-
* the same result sets were two short, frequently-read `session_distill` entries.
|
|
10
|
-
*
|
|
11
|
-
* The correction was abandoned rather than guessed at — writing without the canonical key
|
|
12
|
-
* would have produced a second, parallel memory instead of a revision. So the failure was
|
|
13
|
-
* recorded as an anecdote and nothing more. This file is that anecdote turned into
|
|
14
|
-
* something that can be run.
|
|
15
|
-
*
|
|
16
|
-
* ## What the reproduction shows
|
|
17
|
-
*
|
|
18
|
-
* The fixture below is the shape that failed: one long, older, never-read `cases` entry
|
|
19
|
-
* competing against a dozen short, recent, frequently-read ones, with the long entry
|
|
20
|
-
* holding by far the best raw content score because the query is lifted from its text.
|
|
21
|
-
*
|
|
22
|
-
* Measured against the production `DEFAULT_RETRIEVAL_CONFIG`:
|
|
23
|
-
*
|
|
24
|
-
* raw vector score target 0.920 distractors 0.565–0.620
|
|
25
|
-
* after the chain target 0.469 distractors 0.47–0.71, target ranked 13 of 13
|
|
26
|
-
*
|
|
27
|
-
* The important detail — and it corrects the looser "the threshold cuts candidates down
|
|
28
|
-
* to single digits" reading this was first filed under — is that **nothing filters the
|
|
29
|
-
* target out**. Stage counts show `hard_min_score`, `layer_admission` and `noise_filter`
|
|
30
|
-
* each dropping zero rows; 0.469 clears `hardMinScore` 0.35 comfortably. The entry is
|
|
31
|
-
* admitted and then ranked last, so at any realistic `limit` it is simply never seen.
|
|
32
|
-
*
|
|
33
|
-
* The demotion is cumulative rather than attributable to one stage: importance weight,
|
|
34
|
-
* length normalisation, time decay and the hotness/frequency boosts each move a long,
|
|
35
|
-
* old, unread entry down and a short, fresh, popular one up. An ablation confirmed no
|
|
36
|
-
* single one of them is decisive — restoring any one factor on its own still leaves the
|
|
37
|
-
* target outside the top 5.
|
|
38
|
-
*
|
|
39
|
-
* ## Why these tests assert the failure instead of the fix
|
|
40
|
-
*
|
|
41
|
-
* Repairing the ranking chain is a design-level change that is deliberately out of scope
|
|
42
|
-
* here: it needs a shadow run and a per-row account of what drops out, and the candidate
|
|
43
|
-
* pool widening people reach for first was already measured and rejected
|
|
44
|
-
* (`rejected-recall-widen-candidate-pool`, 2026-08-22). So this is a characterisation
|
|
45
|
-
* test. It pins the current behaviour precisely enough that any change to the chain has
|
|
46
|
-
* to come past it, and it states the property that ought to hold.
|
|
47
|
-
*
|
|
48
|
-
* **If a test in the second block starts failing, that is very likely good news.** It
|
|
49
|
-
* means self-recall improved. Re-read the numbers, then move the assertion into the first
|
|
50
|
-
* block as a real invariant.
|
|
51
|
-
*/
|
|
52
|
-
|
|
53
|
-
import { describe, expect, it } from "bun:test";
|
|
54
|
-
|
|
55
|
-
import { createRetriever, DEFAULT_RETRIEVAL_CONFIG } from "../retriever.js";
|
|
56
|
-
import { TraceCollector } from "../retrieval-trace.js";
|
|
57
|
-
|
|
58
|
-
const DAY_MS = 86_400_000;
|
|
59
|
-
|
|
60
|
-
/** One paragraph of the kind of long case entry that failed — repeated to realistic length. */
|
|
61
|
-
const CASE_PARAGRAPH =
|
|
62
|
-
"冷启动抑制已证伪:hotness 改成 bonus-only 之后,阈值风险队列从 52.25% 降到 33.12%。" +
|
|
63
|
-
"互审拦下三处过度断言,shadow 报告的口径与生产分类器不一致。";
|
|
64
|
-
|
|
65
|
-
const TARGET_TEXT = CASE_PARAGRAPH.repeat(10);
|
|
66
|
-
const TARGET_ID = "4cb4853e-0000-0000-0000-000000000000";
|
|
67
|
-
|
|
68
|
-
/** Lifted word-for-word out of TARGET_TEXT — this is what "its own phrasing" means. */
|
|
69
|
-
const VERBATIM_QUERY = "冷启动抑制已证伪 阈值风险队列 互审拦下三处过度断言 shadow 报告";
|
|
70
|
-
|
|
71
|
-
const DISTRACTOR_COUNT = 12;
|
|
72
|
-
|
|
73
|
-
function buildFixture() {
|
|
74
|
-
const now = Date.now();
|
|
75
|
-
|
|
76
|
-
// The target: long, two months old, never read. Best possible content match.
|
|
77
|
-
const target = {
|
|
78
|
-
id: TARGET_ID,
|
|
79
|
-
text: TARGET_TEXT,
|
|
80
|
-
vector: [1, 0, 0],
|
|
81
|
-
category: "cases",
|
|
82
|
-
scope: "memory",
|
|
83
|
-
importance: 0.75,
|
|
84
|
-
timestamp: now - 60 * DAY_MS,
|
|
85
|
-
metadata: JSON.stringify({
|
|
86
|
-
evolution: {
|
|
87
|
-
status: "active",
|
|
88
|
-
version: 1,
|
|
89
|
-
accessCount: 0,
|
|
90
|
-
lastAccessedAt: null,
|
|
91
|
-
validFrom: now - 60 * DAY_MS,
|
|
92
|
-
validUntil: null,
|
|
93
|
-
},
|
|
94
|
-
boundary: { layer: "durable", authority: "structured-memory", conflictPolicy: "latest-wins" },
|
|
95
|
-
}),
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
// The distractors: short, fresh, heavily read. Weak content match on this query.
|
|
99
|
-
const distractors = Array.from({ length: DISTRACTOR_COUNT }, (_, i) => ({
|
|
100
|
-
id: `dddddddd-${String(i).padStart(4, "0")}-0000-0000-000000000000`,
|
|
101
|
-
text: `会话提炼片段 ${i}:确认了一个与检索质量相关的方向。`,
|
|
102
|
-
vector: [1, 0, 0],
|
|
103
|
-
category: "events",
|
|
104
|
-
scope: "memory",
|
|
105
|
-
importance: 0.9,
|
|
106
|
-
timestamp: now - 2 * DAY_MS,
|
|
107
|
-
metadata: JSON.stringify({
|
|
108
|
-
evolution: {
|
|
109
|
-
status: "active",
|
|
110
|
-
version: 1,
|
|
111
|
-
accessCount: 40,
|
|
112
|
-
lastAccessedAt: now - DAY_MS,
|
|
113
|
-
validFrom: now - 2 * DAY_MS,
|
|
114
|
-
validUntil: null,
|
|
115
|
-
},
|
|
116
|
-
boundary: { layer: "durable", authority: "structured-memory", conflictPolicy: "latest-wins" },
|
|
117
|
-
}),
|
|
118
|
-
}));
|
|
119
|
-
|
|
120
|
-
return [
|
|
121
|
-
{ entry: target, score: 0.92 },
|
|
122
|
-
...distractors.map((entry, i) => ({ entry, score: 0.62 - i * 0.005 })),
|
|
123
|
-
];
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function buildRetriever(candidates: ReturnType<typeof buildFixture>) {
|
|
127
|
-
return createRetriever(
|
|
128
|
-
{
|
|
129
|
-
hasFtsSupport: false,
|
|
130
|
-
async vectorSearch() {
|
|
131
|
-
return candidates;
|
|
132
|
-
},
|
|
133
|
-
} as any,
|
|
134
|
-
{
|
|
135
|
-
async embedQuery() {
|
|
136
|
-
return [1, 0, 0];
|
|
137
|
-
},
|
|
138
|
-
async embedPassage() {
|
|
139
|
-
return [1, 0, 0];
|
|
140
|
-
},
|
|
141
|
-
} as any,
|
|
142
|
-
// Production defaults, minus the reranker (it needs a vendor key and a network call).
|
|
143
|
-
{ ...DEFAULT_RETRIEVAL_CONFIG, rerank: "none" as const },
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async function retrieveWithTrace(limit: number) {
|
|
148
|
-
const candidates = buildFixture();
|
|
149
|
-
const retriever = buildRetriever(candidates);
|
|
150
|
-
const trace = new TraceCollector();
|
|
151
|
-
const results = await retriever.retrieve({
|
|
152
|
-
query: VERBATIM_QUERY,
|
|
153
|
-
scopeFilter: ["memory"],
|
|
154
|
-
limit,
|
|
155
|
-
trace,
|
|
156
|
-
});
|
|
157
|
-
return { results, stages: trace.finalize(VERBATIM_QUERY, "vector").stages };
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// ---------------------------------------------------------------------------
|
|
161
|
-
// Invariants — these must keep holding.
|
|
162
|
-
// ---------------------------------------------------------------------------
|
|
163
|
-
|
|
164
|
-
describe("verbatim self-recall: what the fixture guarantees", () => {
|
|
165
|
-
it("gives the target the strictly best raw content score", () => {
|
|
166
|
-
// Guards the premise. If the fixture ever stops modelling "the query matches this one
|
|
167
|
-
// best", the tests below would be measuring something else entirely.
|
|
168
|
-
const candidates = buildFixture();
|
|
169
|
-
const target = candidates.find((c) => c.entry.id === TARGET_ID);
|
|
170
|
-
const best = Math.max(...candidates.filter((c) => c.entry.id !== TARGET_ID).map((c) => c.score));
|
|
171
|
-
|
|
172
|
-
expect(target?.score).toBe(0.92);
|
|
173
|
-
expect(target!.score).toBeGreaterThan(best);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it("admits the target through every gate — it is not filtered out", async () => {
|
|
177
|
-
const { stages } = await retrieveWithTrace(DISTRACTOR_COUNT + 1);
|
|
178
|
-
|
|
179
|
-
// This is the part that must never regress into an actual filter. Losing a
|
|
180
|
-
// verbatim-matching memory to a ranking is bad; dropping it at a threshold would be
|
|
181
|
-
// worse, and would need a different fix.
|
|
182
|
-
for (const name of ["hard_min_score", "noise_filter"]) {
|
|
183
|
-
const stage = stages.find((s) => s.name === name);
|
|
184
|
-
expect(stage, `stage ${name} missing from trace`).toBeDefined();
|
|
185
|
-
expect(stage?.droppedCount, `stage ${name} dropped rows`).toBe(0);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// `layer_admission` is env-gated: applyLayerAdmission returns before touching the
|
|
189
|
-
// trace when RECALLNEST_LAYER_ADMISSION is off, which is the default. Asserting it
|
|
190
|
-
// unconditionally would only be asserting the developer's local .env — so check it
|
|
191
|
-
// when it ran, and rely on the whole-result assertion below when it did not.
|
|
192
|
-
const admission = stages.find((s) => s.name === "layer_admission");
|
|
193
|
-
if (admission) expect(admission.droppedCount, "layer admission dropped rows").toBe(0);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it("returns every candidate — nothing is removed on the way through", async () => {
|
|
197
|
-
// The environment-independent form of the claim above: whatever stages ran, the
|
|
198
|
-
// result set still holds all of them, so no gate removed the target.
|
|
199
|
-
const { results } = await retrieveWithTrace(DISTRACTOR_COUNT + 1);
|
|
200
|
-
|
|
201
|
-
expect(results).toHaveLength(DISTRACTOR_COUNT + 1);
|
|
202
|
-
expect(results.some((r) => r.entry.id === TARGET_ID)).toBe(true);
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it("keeps the target's final score above the hard minimum", async () => {
|
|
206
|
-
const { results } = await retrieveWithTrace(DISTRACTOR_COUNT + 1);
|
|
207
|
-
const target = results.find((r) => r.entry.id === TARGET_ID);
|
|
208
|
-
|
|
209
|
-
expect(target).toBeDefined();
|
|
210
|
-
expect(target!.score).toBeGreaterThan(DEFAULT_RETRIEVAL_CONFIG.hardMinScore);
|
|
211
|
-
});
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
// ---------------------------------------------------------------------------
|
|
215
|
-
// The defect — currently reproduced. A failure here probably means it got better.
|
|
216
|
-
// ---------------------------------------------------------------------------
|
|
217
|
-
|
|
218
|
-
describe("verbatim self-recall: the reported failure, reproduced", () => {
|
|
219
|
-
it("does not return the target in a default-sized result window", async () => {
|
|
220
|
-
// The original report in one line: eleven recall slots across two calls, queries taken
|
|
221
|
-
// from the entry's own text, target never returned.
|
|
222
|
-
const { results } = await retrieveWithTrace(5);
|
|
223
|
-
|
|
224
|
-
expect(results.some((r) => r.entry.id === TARGET_ID)).toBe(false);
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it("still does not return it when the window is doubled", async () => {
|
|
228
|
-
// Widening the window is the obvious first reflex, so it is worth recording that it
|
|
229
|
-
// does not work here either — the entry is last, not marginal.
|
|
230
|
-
const { results } = await retrieveWithTrace(10);
|
|
231
|
-
|
|
232
|
-
expect(results.some((r) => r.entry.id === TARGET_ID)).toBe(false);
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it("ranks the best content match last once every candidate is returned", async () => {
|
|
236
|
-
const { results } = await retrieveWithTrace(DISTRACTOR_COUNT + 1);
|
|
237
|
-
const rank = results.findIndex((r) => r.entry.id === TARGET_ID);
|
|
238
|
-
|
|
239
|
-
expect(results).toHaveLength(DISTRACTOR_COUNT + 1);
|
|
240
|
-
// Went in first by a wide margin, comes out last: a complete inversion of the
|
|
241
|
-
// content signal by the weighting stages that follow it.
|
|
242
|
-
expect(rank).toBe(DISTRACTOR_COUNT);
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
it("shows the inversion is cumulative, not caused by one stage", async () => {
|
|
246
|
-
// Ablation summary: restoring any single disadvantage on its own — importance,
|
|
247
|
-
// recency, length, access count — still leaves the target out of the top 5. Recorded
|
|
248
|
-
// here so nobody re-runs the search for a single culprit.
|
|
249
|
-
const candidates = buildFixture();
|
|
250
|
-
const now = Date.now();
|
|
251
|
-
|
|
252
|
-
const variants: Array<[string, () => typeof candidates]> = [
|
|
253
|
-
["importance raised to match", () =>
|
|
254
|
-
candidates.map((c) => c.entry.id === TARGET_ID
|
|
255
|
-
? { ...c, entry: { ...c.entry, importance: 0.9 } }
|
|
256
|
-
: c)],
|
|
257
|
-
["age reduced to match", () =>
|
|
258
|
-
candidates.map((c) => c.entry.id === TARGET_ID
|
|
259
|
-
? { ...c, entry: { ...c.entry, timestamp: now - 2 * DAY_MS } }
|
|
260
|
-
: c)],
|
|
261
|
-
["text shortened", () =>
|
|
262
|
-
candidates.map((c) => c.entry.id === TARGET_ID
|
|
263
|
-
? { ...c, entry: { ...c.entry, text: CASE_PARAGRAPH } }
|
|
264
|
-
: c)],
|
|
265
|
-
];
|
|
266
|
-
|
|
267
|
-
for (const [label, build] of variants) {
|
|
268
|
-
const results = await buildRetriever(build()).retrieve({
|
|
269
|
-
query: VERBATIM_QUERY,
|
|
270
|
-
scopeFilter: ["memory"],
|
|
271
|
-
limit: 5,
|
|
272
|
-
});
|
|
273
|
-
expect(
|
|
274
|
-
results.some((r) => r.entry.id === TARGET_ID),
|
|
275
|
-
`${label}: target unexpectedly recalled — the ablation result changed`,
|
|
276
|
-
).toBe(false);
|
|
277
|
-
}
|
|
278
|
-
});
|
|
279
|
-
});
|
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for Tier 3.3: Version Coexistence + Competition
|
|
3
|
-
*
|
|
4
|
-
* Validates:
|
|
5
|
-
* 1. computeVersionRank formula
|
|
6
|
-
* 2. createVersionGroup tags both entries with same group
|
|
7
|
-
* 3. deduplicateByVersionGroup keeps top-ranked per group
|
|
8
|
-
* 4. Entries without version_group pass through unchanged
|
|
9
|
-
* 5. Multiple version groups are handled independently
|
|
10
|
-
* 6. Existing version_group is reused (not duplicated)
|
|
11
|
-
*/
|
|
12
|
-
import { describe, expect, it } from "bun:test";
|
|
13
|
-
import {
|
|
14
|
-
computeVersionRank,
|
|
15
|
-
deduplicateByVersionGroup,
|
|
16
|
-
createVersionGroup,
|
|
17
|
-
clearVersionGroupMetadata,
|
|
18
|
-
planSingletonVersionGroupRepairs,
|
|
19
|
-
} from "../version-manager.js";
|
|
20
|
-
import type { MemoryEntry } from "../store.js";
|
|
21
|
-
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
// Helpers
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
|
|
26
|
-
function makeEntry(
|
|
27
|
-
id: string,
|
|
28
|
-
overrides: {
|
|
29
|
-
confidence?: number;
|
|
30
|
-
accessCount?: number;
|
|
31
|
-
version_group?: string;
|
|
32
|
-
version_rank?: number;
|
|
33
|
-
} = {},
|
|
34
|
-
): MemoryEntry {
|
|
35
|
-
const meta: Record<string, unknown> = {
|
|
36
|
-
confidence: overrides.confidence ?? 0.7,
|
|
37
|
-
accessCount: overrides.accessCount ?? 0,
|
|
38
|
-
};
|
|
39
|
-
if (overrides.version_group) meta.version_group = overrides.version_group;
|
|
40
|
-
if (overrides.version_rank !== undefined) meta.version_rank = overrides.version_rank;
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
id,
|
|
44
|
-
text: `memory-${id}`,
|
|
45
|
-
vector: [1, 0, 0],
|
|
46
|
-
category: "events",
|
|
47
|
-
scope: "project:test",
|
|
48
|
-
importance: 0.7,
|
|
49
|
-
timestamp: Date.now(),
|
|
50
|
-
metadata: JSON.stringify(meta),
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function createMockStore(entries: MemoryEntry[]) {
|
|
55
|
-
const data = new Map(entries.map(e => [e.id, { ...e }]));
|
|
56
|
-
const updates: Array<{ id: string; metadata: string }> = [];
|
|
57
|
-
|
|
58
|
-
return {
|
|
59
|
-
data,
|
|
60
|
-
updates,
|
|
61
|
-
async getById(id: string) {
|
|
62
|
-
return data.get(id) ?? null;
|
|
63
|
-
},
|
|
64
|
-
async update(id: string, upd: { metadata?: string }, _scope?: string[]) {
|
|
65
|
-
const entry = data.get(id);
|
|
66
|
-
if (!entry) return null;
|
|
67
|
-
if (upd.metadata) {
|
|
68
|
-
entry.metadata = upd.metadata;
|
|
69
|
-
updates.push({ id, metadata: upd.metadata });
|
|
70
|
-
}
|
|
71
|
-
return entry;
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// ---------------------------------------------------------------------------
|
|
77
|
-
// computeVersionRank
|
|
78
|
-
// ---------------------------------------------------------------------------
|
|
79
|
-
|
|
80
|
-
describe("computeVersionRank", () => {
|
|
81
|
-
it("computes rank from importance, confidence, and accessCount", () => {
|
|
82
|
-
const entry = makeEntry("a", { confidence: 1.0, accessCount: 10 });
|
|
83
|
-
const rank = computeVersionRank(entry);
|
|
84
|
-
// quality = 0.5*0.7 + 0.5*1.0 = 0.85 (importance=0.7 from makeEntry)
|
|
85
|
-
// rank = 0.85 * (1 + ln(11)) ≈ 0.85 * 3.398 = 2.888
|
|
86
|
-
expect(rank).toBeGreaterThan(2.8);
|
|
87
|
-
expect(rank).toBeLessThan(3.0);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("returns baseline for default confidence and zero access", () => {
|
|
91
|
-
const entry = makeEntry("b", { confidence: 0.7, accessCount: 0 });
|
|
92
|
-
const rank = computeVersionRank(entry);
|
|
93
|
-
// quality = 0.5*0.7 + 0.5*0.7 = 0.7
|
|
94
|
-
// rank = 0.7 * (1 + 0) = 0.7
|
|
95
|
-
expect(rank).toBeCloseTo(0.7, 5);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("higher confidence wins over lower with same access", () => {
|
|
99
|
-
const high = computeVersionRank(makeEntry("h", { confidence: 1.0, accessCount: 5 }));
|
|
100
|
-
const low = computeVersionRank(makeEntry("l", { confidence: 0.3, accessCount: 5 }));
|
|
101
|
-
expect(high).toBeGreaterThan(low);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("higher access wins over lower with same confidence", () => {
|
|
105
|
-
const frequent = computeVersionRank(makeEntry("f", { confidence: 0.7, accessCount: 20 }));
|
|
106
|
-
const rare = computeVersionRank(makeEntry("r", { confidence: 0.7, accessCount: 1 }));
|
|
107
|
-
expect(frequent).toBeGreaterThan(rare);
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// ---------------------------------------------------------------------------
|
|
112
|
-
// createVersionGroup
|
|
113
|
-
// ---------------------------------------------------------------------------
|
|
114
|
-
|
|
115
|
-
describe("createVersionGroup", () => {
|
|
116
|
-
it("tags both entries with the same version_group", async () => {
|
|
117
|
-
const canonical = makeEntry("canon", { confidence: 1.0, accessCount: 5 });
|
|
118
|
-
const member = makeEntry("member", { confidence: 0.5, accessCount: 1 });
|
|
119
|
-
const store = createMockStore([canonical, member]);
|
|
120
|
-
|
|
121
|
-
const groupId = await createVersionGroup(store as any, canonical, member, "project:test");
|
|
122
|
-
|
|
123
|
-
expect(groupId).toBeTruthy();
|
|
124
|
-
expect(store.updates.length).toBe(2);
|
|
125
|
-
|
|
126
|
-
const canonMeta = JSON.parse(store.data.get("canon")!.metadata);
|
|
127
|
-
const memberMeta = JSON.parse(store.data.get("member")!.metadata);
|
|
128
|
-
|
|
129
|
-
expect(canonMeta.version_group).toBe(groupId);
|
|
130
|
-
expect(memberMeta.version_group).toBe(groupId);
|
|
131
|
-
expect(canonMeta.version_rank).toBeGreaterThan(memberMeta.version_rank);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it("reuses existing version_group from canonical", async () => {
|
|
135
|
-
const canonical = makeEntry("canon", {
|
|
136
|
-
confidence: 1.0,
|
|
137
|
-
accessCount: 5,
|
|
138
|
-
version_group: "vg-existing",
|
|
139
|
-
version_rank: 3.0,
|
|
140
|
-
});
|
|
141
|
-
const member = makeEntry("new", { confidence: 0.5, accessCount: 0 });
|
|
142
|
-
const store = createMockStore([canonical, member]);
|
|
143
|
-
|
|
144
|
-
const groupId = await createVersionGroup(store as any, canonical, member, "project:test");
|
|
145
|
-
|
|
146
|
-
expect(groupId).toBe("vg-existing");
|
|
147
|
-
const memberMeta = JSON.parse(store.data.get("new")!.metadata);
|
|
148
|
-
expect(memberMeta.version_group).toBe("vg-existing");
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
// ---------------------------------------------------------------------------
|
|
153
|
-
// deduplicateByVersionGroup
|
|
154
|
-
// ---------------------------------------------------------------------------
|
|
155
|
-
|
|
156
|
-
describe("deduplicateByVersionGroup", () => {
|
|
157
|
-
it("keeps only the top-ranked entry per version group", () => {
|
|
158
|
-
const results = [
|
|
159
|
-
{ entry: makeEntry("a", { version_group: "vg1", version_rank: 3.0 }), score: 0.9, sources: {} },
|
|
160
|
-
{ entry: makeEntry("b", { version_group: "vg1", version_rank: 1.0 }), score: 0.85, sources: {} },
|
|
161
|
-
{ entry: makeEntry("c"), score: 0.8, sources: {} },
|
|
162
|
-
];
|
|
163
|
-
|
|
164
|
-
const deduped = deduplicateByVersionGroup(results);
|
|
165
|
-
|
|
166
|
-
expect(deduped.length).toBe(2);
|
|
167
|
-
expect(deduped.map(r => r.entry.id)).toContain("a");
|
|
168
|
-
expect(deduped.map(r => r.entry.id)).toContain("c");
|
|
169
|
-
expect(deduped.map(r => r.entry.id)).not.toContain("b");
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it("passes through entries without version_group", () => {
|
|
173
|
-
const results = [
|
|
174
|
-
{ entry: makeEntry("x"), score: 0.9, sources: {} },
|
|
175
|
-
{ entry: makeEntry("y"), score: 0.8, sources: {} },
|
|
176
|
-
];
|
|
177
|
-
|
|
178
|
-
const deduped = deduplicateByVersionGroup(results);
|
|
179
|
-
|
|
180
|
-
expect(deduped.length).toBe(2);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it("handles multiple independent version groups", () => {
|
|
184
|
-
const results = [
|
|
185
|
-
{ entry: makeEntry("a1", { version_group: "vg1", version_rank: 3.0 }), score: 0.95, sources: {} },
|
|
186
|
-
{ entry: makeEntry("a2", { version_group: "vg1", version_rank: 1.0 }), score: 0.90, sources: {} },
|
|
187
|
-
{ entry: makeEntry("b1", { version_group: "vg2", version_rank: 2.0 }), score: 0.85, sources: {} },
|
|
188
|
-
{ entry: makeEntry("b2", { version_group: "vg2", version_rank: 4.0 }), score: 0.80, sources: {} },
|
|
189
|
-
{ entry: makeEntry("solo"), score: 0.75, sources: {} },
|
|
190
|
-
];
|
|
191
|
-
|
|
192
|
-
const deduped = deduplicateByVersionGroup(results);
|
|
193
|
-
|
|
194
|
-
expect(deduped.length).toBe(3);
|
|
195
|
-
const ids = deduped.map(r => r.entry.id);
|
|
196
|
-
expect(ids).toContain("a1"); // vg1 winner (rank 3.0)
|
|
197
|
-
expect(ids).toContain("b2"); // vg2 winner (rank 4.0)
|
|
198
|
-
expect(ids).toContain("solo");
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it("handles empty array", () => {
|
|
202
|
-
expect(deduplicateByVersionGroup([])).toEqual([]);
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it("handles single entry in a version group", () => {
|
|
206
|
-
const results = [
|
|
207
|
-
{ entry: makeEntry("only", { version_group: "vg1", version_rank: 2.0 }), score: 0.9, sources: {} },
|
|
208
|
-
];
|
|
209
|
-
|
|
210
|
-
const deduped = deduplicateByVersionGroup(results);
|
|
211
|
-
|
|
212
|
-
expect(deduped.length).toBe(1);
|
|
213
|
-
expect(deduped[0].entry.id).toBe("only");
|
|
214
|
-
});
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
describe("singleton version-group repair", () => {
|
|
218
|
-
it("plans only groups with one surviving member", () => {
|
|
219
|
-
const singleton = makeEntry("solo", { version_group: "vg-single", version_rank: 1 });
|
|
220
|
-
const pairA = makeEntry("pair-a", { version_group: "vg-pair", version_rank: 2 });
|
|
221
|
-
const pairB = makeEntry("pair-b", { version_group: "vg-pair", version_rank: 1 });
|
|
222
|
-
const malformed = makeEntry("bad");
|
|
223
|
-
malformed.metadata = "{not-json";
|
|
224
|
-
|
|
225
|
-
expect(planSingletonVersionGroupRepairs([
|
|
226
|
-
singleton,
|
|
227
|
-
pairA,
|
|
228
|
-
pairB,
|
|
229
|
-
makeEntry("ungrouped"),
|
|
230
|
-
malformed,
|
|
231
|
-
])).toEqual([{ id: "solo", groupId: "vg-single" }]);
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
it("clears only version-group keys and preserves unrelated metadata", () => {
|
|
235
|
-
const meta: Record<string, unknown> = {
|
|
236
|
-
version_group: "vg-single",
|
|
237
|
-
version_rank: 2.3,
|
|
238
|
-
version_created: "2026-07-07T00:00:00.000Z",
|
|
239
|
-
confidence: 0.9,
|
|
240
|
-
evolution: { status: "active" },
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
expect(clearVersionGroupMetadata(meta, "vg-single")).toEqual({
|
|
244
|
-
confidence: 0.9,
|
|
245
|
-
evolution: { status: "active" },
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
});
|