recallnest 1.4.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/.claude-plugin/CLAUDE.md +29 -0
- package/.claude-plugin/marketplace.json +18 -0
- package/.env.example +3 -0
- package/.github/workflows/ci.yml +28 -0
- package/.mcp.json +9 -0
- package/CHANGELOG.md +51 -0
- package/CLAUDE.md +42 -0
- package/DEMO.md +94 -0
- package/EVAL.md +89 -0
- package/FAILURES.md +75 -0
- package/LICENSE +21 -0
- package/OPERATIONS.md +59 -0
- package/README.md +427 -0
- package/README_CN.md +425 -0
- package/ROADMAP.md +198 -0
- package/assets/recallnest-workbench.png +0 -0
- package/assets/ui/app.js +807 -0
- package/assets/ui/index.html +154 -0
- package/assets/ui/styles.css +605 -0
- package/assets/wechat-qr.jpg +0 -0
- package/config.json.example +38 -0
- package/daily-recall.sh +87 -0
- package/daily-reflections/2026-03-05.md +15 -0
- package/daily-reflections/2026-03-06.md +15 -0
- package/daily-reflections/2026-03-07.md +7 -0
- package/daily-reflections/2026-03-08.md +34 -0
- package/daily-reflections/2026-03-09.md +35 -0
- package/daily-reflections/2026-03-10.md +36 -0
- package/daily-reflections/2026-03-11.md +35 -0
- package/daily-reflections/2026-03-12.md +36 -0
- package/daily-reflections/2026-03-13.md +35 -0
- package/daily-reflections/2026-03-14.md +35 -0
- package/daily-reflections/2026-03-15.md +35 -0
- package/daily-reflections/2026-03-16.md +41 -0
- package/daily-reflections/2026-03-17.md +10 -0
- package/daily-reflections/2026-03-18.md +10 -0
- package/daily-reflections/2026-03-19.md +10 -0
- package/daily-reflections/2026-03-20.md +10 -0
- package/daily-reflections/2026-03-21.md +10 -0
- package/daily-reflections/2026-03-22.md +10 -0
- package/daily-reflections/2026-03-23.md +10 -0
- package/daily-reflections/2026-03-24.md +10 -0
- package/daily-reflections/2026-03-25.md +32 -0
- package/daily-reflections/2026-03-26.md +37 -0
- package/daily-reflections/2026-03-27.md +35 -0
- package/daily-reflections/2026-03-28.md +35 -0
- package/daily-reflections/2026-03-29.md +35 -0
- package/daily-reflections/2026-03-30.md +36 -0
- package/daily-reflections/2026-03-31.md +35 -0
- package/daily-reflections/2026-04-01.md +35 -0
- package/docs/api-reference.md +1291 -0
- package/docs/architecture.md +168 -0
- package/docs/memory-boundary-contract.md +226 -0
- package/docs/memory-categories.md +119 -0
- package/docs/pipeline-audit-ultramemory.md +177 -0
- package/docs/plans/2026-03-08-query-expansion.md +270 -0
- package/docs/plans/2026-03-16-continuity-layer.md +196 -0
- package/docs/plans/mcp-tiering-plan.md +126 -0
- package/docs/research/recall-quality-audit-2026-03-22.md +153 -0
- package/docs/self-evolution.md +198 -0
- package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +782 -0
- package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +77 -0
- package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +95 -0
- package/eval/cases.json +85 -0
- package/eval/continuity/case-seeds.json +90 -0
- package/eval/continuity/cases.json +302 -0
- package/eval/continuity/memory-seeds.json +26 -0
- package/eval/continuity/pattern-seeds.json +100 -0
- package/eval/continuity/results-2026-03-16.md +117 -0
- package/eval/continuity/results-2026-03-19.md +453 -0
- package/eval/continuity/runbook.md +110 -0
- package/eval/continuity/seed-cases.md +54 -0
- package/eval/reports/2026-03-06-abstract-query-baseline.md +96 -0
- package/eval/reports/2026-03-06-aws-ssh-baseline.md +82 -0
- package/eval/reports/2026-03-06-baseline.md +82 -0
- package/eval/reports/query-expansion.md +138 -0
- package/integrations/README.md +57 -0
- package/integrations/claude-code/README.md +133 -0
- package/integrations/claude-code/claude-md-snippet.md +31 -0
- package/integrations/claude-code/setup.sh +66 -0
- package/integrations/claude-code/smoke-continuity.sh +452 -0
- package/integrations/codex/README.md +49 -0
- package/integrations/codex/agents-md-snippet.md +31 -0
- package/integrations/codex/setup.sh +58 -0
- package/integrations/examples/claude-agent-sdk/memory-agent.ts +182 -0
- package/integrations/examples/langchain/memory-chain.py +111 -0
- package/integrations/examples/openai-agents-sdk/memory-agent.py +109 -0
- package/integrations/gemini-cli/README.md +57 -0
- package/integrations/gemini-cli/gemini-md-snippet.md +31 -0
- package/integrations/gemini-cli/setup.sh +64 -0
- package/integrations/shared/install-managed-block.sh +51 -0
- package/lm +19 -0
- package/package.json +50 -0
- package/plan-v1.2-distributable.md +105 -0
- package/scripts/archive-distilled-junk.ts +116 -0
- package/scripts/backfill-l0l1l2.ts +98 -0
- package/scripts/blind-scan.ts +588 -0
- package/scripts/cleanup-step1-exact-dups.ts +117 -0
- package/scripts/cleanup-step2-template-dups.ts +113 -0
- package/scripts/cleanup-step3-stale-content.ts +149 -0
- package/scripts/cleanup-step4-orphan-scopes.ts +131 -0
- package/scripts/delete-distilled-junk.ts +88 -0
- package/scripts/distill-facts.ts +305 -0
- package/scripts/health-check.ts +343 -0
- package/scripts/incremental-ingest.sh +71 -0
- package/scripts/mlp-health-check.ts +382 -0
- package/scripts/pull-claude-desktop.ts +515 -0
- package/scripts/re-ingest-cc.sh +66 -0
- package/scripts/rebalance-memory-health.ts +181 -0
- package/scripts/run-desktop-ingest.sh +11 -0
- package/scripts/start-server.sh +12 -0
- package/scripts/viz-dashboard.html +423 -0
- package/scripts/viz-export.ts +703 -0
- package/src/__tests__/access-tracker-novelty.test.ts +228 -0
- package/src/__tests__/auto-consolidation.test.ts +168 -0
- package/src/__tests__/auto-recall.test.ts +131 -0
- package/src/__tests__/batch-dedup.test.ts +65 -0
- package/src/__tests__/capture-engine.test.ts +1142 -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 +33 -0
- package/src/__tests__/confidence-tracker.test.ts +196 -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 +374 -0
- package/src/__tests__/consolidation-engine.test.ts +181 -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-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__/core-recall-eval.test.ts +235 -0
- package/src/__tests__/core-summary.test.ts +109 -0
- package/src/__tests__/dedup-actions.test.ts +58 -0
- package/src/__tests__/deterministic-id.test.ts +39 -0
- package/src/__tests__/doctor.test.ts +303 -0
- package/src/__tests__/drill-down.test.ts +108 -0
- package/src/__tests__/embedder.test.ts +122 -0
- package/src/__tests__/entity-resolver.test.ts +58 -0
- package/src/__tests__/event-segmenter.test.ts +153 -0
- package/src/__tests__/hotness-blend.test.ts +76 -0
- package/src/__tests__/ingest.test.ts +360 -0
- package/src/__tests__/kg-extractor.test.ts +184 -0
- package/src/__tests__/kg-store.test.ts +198 -0
- package/src/__tests__/llm-consolidation.test.ts +226 -0
- package/src/__tests__/memory-boundaries.test.ts +237 -0
- package/src/__tests__/memory-health-rebalance.test.ts +144 -0
- package/src/__tests__/memory-output.test.ts +215 -0
- package/src/__tests__/multi-vector.test.ts +242 -0
- package/src/__tests__/ppr-traversal.test.ts +256 -0
- package/src/__tests__/preference-guard-smoke.test.ts +211 -0
- package/src/__tests__/preference-matcher.test.ts +230 -0
- package/src/__tests__/preference-slots.test.ts +106 -0
- package/src/__tests__/prospective-memory.test.ts +283 -0
- package/src/__tests__/query-entity-detector.test.ts +171 -0
- package/src/__tests__/query-expander.test.ts +34 -0
- package/src/__tests__/recall-mode.test.ts +64 -0
- package/src/__tests__/result-synthesizer.test.ts +198 -0
- package/src/__tests__/resume-context.test.ts +4743 -0
- package/src/__tests__/retrieval-trace.test.ts +213 -0
- package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
- package/src/__tests__/retroactive-boost.test.ts +223 -0
- package/src/__tests__/scope-inventory.test.ts +200 -0
- package/src/__tests__/scope-policy.test.ts +69 -0
- package/src/__tests__/session-checkpoint.test.ts +259 -0
- package/src/__tests__/session-gc.test.ts +215 -0
- package/src/__tests__/store-scope-audit.test.ts +81 -0
- package/src/__tests__/temporal-parser.test.ts +241 -0
- package/src/__tests__/term-registry.test.ts +121 -0
- package/src/__tests__/tool-output-compressor.test.ts +133 -0
- package/src/__tests__/version-manager.test.ts +213 -0
- package/src/__tests__/workflow-observation.test.ts +400 -0
- package/src/access-tracker.ts +316 -0
- package/src/adaptive-retrieval.ts +64 -0
- package/src/api-server.ts +667 -0
- package/src/asset-sync.ts +79 -0
- package/src/auto-consolidation.ts +133 -0
- package/src/auto-gc.ts +124 -0
- package/src/auto-recall.ts +95 -0
- package/src/capture-engine.ts +931 -0
- package/src/capture-heuristic.ts +177 -0
- package/src/cascade-forget.ts +135 -0
- package/src/chunker.ts +253 -0
- package/src/cli.ts +1917 -0
- package/src/confidence-tracker.ts +166 -0
- package/src/conflict-advisor.ts +433 -0
- package/src/conflict-engine.ts +181 -0
- package/src/conflict-escalation.ts +138 -0
- package/src/conflict-lifecycle.ts +67 -0
- package/src/conflict-output.ts +218 -0
- package/src/conflict-schema.ts +130 -0
- package/src/conflict-store.ts +127 -0
- package/src/consolidation-engine.ts +256 -0
- package/src/context-composer-pins.ts +183 -0
- package/src/context-composer-queries.ts +121 -0
- package/src/context-composer-scope.ts +56 -0
- package/src/context-composer-stable-selection.ts +200 -0
- package/src/context-composer-stable.ts +217 -0
- package/src/context-composer-task-fallbacks.ts +36 -0
- package/src/context-composer-task-ranking.ts +197 -0
- package/src/context-composer-task-results.ts +137 -0
- package/src/context-composer-task-selection.ts +342 -0
- package/src/context-composer-text.ts +57 -0
- package/src/context-composer.ts +300 -0
- package/src/context-renderer.ts +179 -0
- package/src/decay-engine.ts +189 -0
- package/src/doctor.ts +543 -0
- package/src/embedder.ts +557 -0
- package/src/entity-resolver.ts +131 -0
- package/src/eval.ts +632 -0
- package/src/event-segmenter.ts +216 -0
- package/src/ingest.ts +1822 -0
- package/src/kg-extractor.ts +217 -0
- package/src/kg-store.ts +353 -0
- package/src/legacy-scope-review.ts +53 -0
- package/src/llm-client.ts +629 -0
- package/src/llm-consolidation.ts +168 -0
- package/src/mcp-server.ts +1337 -0
- package/src/memory-assets.ts +475 -0
- package/src/memory-boundaries.ts +354 -0
- package/src/memory-health-rebalance.ts +220 -0
- package/src/memory-output.ts +474 -0
- package/src/memory-schema.ts +184 -0
- package/src/multi-vector.ts +234 -0
- package/src/noise-filter.ts +155 -0
- package/src/ppr-traversal.ts +279 -0
- package/src/preference-matcher.ts +174 -0
- package/src/preference-slots.ts +259 -0
- package/src/prospective-memory.ts +198 -0
- package/src/query-entity-detector.ts +207 -0
- package/src/query-expander.ts +98 -0
- package/src/result-synthesizer.ts +114 -0
- package/src/retrieval-profiles.ts +101 -0
- package/src/retrieval-trace.ts +94 -0
- package/src/retriever.ts +1338 -0
- package/src/retroactive-boost.ts +154 -0
- package/src/rif.ts +73 -0
- package/src/runtime-config.ts +172 -0
- package/src/schema-utils.ts +56 -0
- package/src/scope-inventory.ts +310 -0
- package/src/scope-policy.ts +142 -0
- package/src/session-engine.ts +168 -0
- package/src/session-output.ts +96 -0
- package/src/session-schema.ts +60 -0
- package/src/session-store.ts +293 -0
- package/src/stderr-log.ts +16 -0
- package/src/store.ts +903 -0
- package/src/temporal-parser.ts +318 -0
- package/src/term-registry.ts +791 -0
- package/src/tool-output-compressor.ts +115 -0
- package/src/tracker.ts +57 -0
- package/src/ui-server.ts +430 -0
- package/src/version-manager.ts +172 -0
- package/src/workflow-observation-engine.ts +316 -0
- package/src/workflow-observation-managed.ts +83 -0
- package/src/workflow-observation-output.ts +86 -0
- package/src/workflow-observation-schema.ts +87 -0
- package/src/workflow-observation-scope-review.ts +409 -0
- package/src/workflow-observation-store.ts +69 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
import { summarizeConflictAdvice } from "../conflict-advisor.js";
|
|
7
|
+
import { buildConflictCandidateRecord, resolveConflictCandidate } from "../conflict-engine.js";
|
|
8
|
+
import { ConflictCandidateStore } from "../conflict-store.js";
|
|
9
|
+
|
|
10
|
+
const tempDirs: string[] = [];
|
|
11
|
+
|
|
12
|
+
function createTempConflictStore(): ConflictCandidateStore {
|
|
13
|
+
const dir = mkdtempSync(join(tmpdir(), "recallnest-conflicts-"));
|
|
14
|
+
tempDirs.push(dir);
|
|
15
|
+
return new ConflictCandidateStore(dir);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
while (tempDirs.length > 0) {
|
|
20
|
+
const dir = tempDirs.pop();
|
|
21
|
+
if (dir) rmSync(dir, { recursive: true, force: true });
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
function createConflictRecord() {
|
|
26
|
+
return buildConflictCandidateRecord({
|
|
27
|
+
canonicalKey: "user-reply-style",
|
|
28
|
+
category: "preferences",
|
|
29
|
+
fingerprint: "user-reply-style--durable-1--source-1--new-text",
|
|
30
|
+
reason: "promotion_conflicts_with_existing_durable",
|
|
31
|
+
existing: {
|
|
32
|
+
memoryId: "durable-1",
|
|
33
|
+
text: "User prefers concise, direct replies.",
|
|
34
|
+
category: "preferences",
|
|
35
|
+
scope: "memory:agent",
|
|
36
|
+
importance: 0.84,
|
|
37
|
+
metadata: JSON.stringify({
|
|
38
|
+
source: "agent",
|
|
39
|
+
canonicalKey: "user-reply-style",
|
|
40
|
+
boundary: {
|
|
41
|
+
layer: "durable",
|
|
42
|
+
authority: "structured-memory",
|
|
43
|
+
conflictPolicy: "latest-wins",
|
|
44
|
+
originalCategory: "preferences",
|
|
45
|
+
},
|
|
46
|
+
}),
|
|
47
|
+
canonicalKey: "user-reply-style",
|
|
48
|
+
},
|
|
49
|
+
incoming: {
|
|
50
|
+
text: "User prefers colloquial writing that stays grounded and non-salesy.",
|
|
51
|
+
category: "preferences",
|
|
52
|
+
scope: "memory:agent",
|
|
53
|
+
importance: 0.78,
|
|
54
|
+
metadata: JSON.stringify({
|
|
55
|
+
source: "agent",
|
|
56
|
+
canonicalKey: "user-reply-style",
|
|
57
|
+
boundary: {
|
|
58
|
+
layer: "durable",
|
|
59
|
+
authority: "structured-memory",
|
|
60
|
+
conflictPolicy: "latest-wins",
|
|
61
|
+
originalCategory: "preferences",
|
|
62
|
+
},
|
|
63
|
+
promotedFrom: {
|
|
64
|
+
memoryId: "source-1",
|
|
65
|
+
scope: "cc:session-1",
|
|
66
|
+
category: "events",
|
|
67
|
+
},
|
|
68
|
+
}),
|
|
69
|
+
source: "agent",
|
|
70
|
+
sourceMemoryId: "source-1",
|
|
71
|
+
sourceCategory: "events",
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function createMergeableConflictRecord() {
|
|
77
|
+
return buildConflictCandidateRecord({
|
|
78
|
+
canonicalKey: "user-reply-style",
|
|
79
|
+
category: "preferences",
|
|
80
|
+
fingerprint: "user-reply-style--durable-1--source-1--mergeable",
|
|
81
|
+
reason: "promotion_conflicts_with_existing_durable",
|
|
82
|
+
existing: {
|
|
83
|
+
memoryId: "durable-1",
|
|
84
|
+
text: "User prefers concise grounded technical replies; avoid sales language.",
|
|
85
|
+
category: "preferences",
|
|
86
|
+
scope: "memory:agent",
|
|
87
|
+
importance: 0.84,
|
|
88
|
+
metadata: JSON.stringify({
|
|
89
|
+
source: "agent",
|
|
90
|
+
canonicalKey: "user-reply-style",
|
|
91
|
+
boundary: {
|
|
92
|
+
layer: "durable",
|
|
93
|
+
authority: "structured-memory",
|
|
94
|
+
conflictPolicy: "latest-wins",
|
|
95
|
+
originalCategory: "preferences",
|
|
96
|
+
},
|
|
97
|
+
}),
|
|
98
|
+
canonicalKey: "user-reply-style",
|
|
99
|
+
},
|
|
100
|
+
incoming: {
|
|
101
|
+
text: "User prefers concise grounded technical replies; keep the tone colloquial.",
|
|
102
|
+
category: "preferences",
|
|
103
|
+
scope: "memory:agent",
|
|
104
|
+
importance: 0.78,
|
|
105
|
+
metadata: JSON.stringify({
|
|
106
|
+
source: "agent",
|
|
107
|
+
canonicalKey: "user-reply-style",
|
|
108
|
+
boundary: {
|
|
109
|
+
layer: "durable",
|
|
110
|
+
authority: "structured-memory",
|
|
111
|
+
conflictPolicy: "latest-wins",
|
|
112
|
+
originalCategory: "preferences",
|
|
113
|
+
},
|
|
114
|
+
promotedFrom: {
|
|
115
|
+
memoryId: "source-1",
|
|
116
|
+
scope: "cc:session-1",
|
|
117
|
+
category: "events",
|
|
118
|
+
},
|
|
119
|
+
}),
|
|
120
|
+
source: "agent",
|
|
121
|
+
sourceMemoryId: "source-1",
|
|
122
|
+
sourceCategory: "events",
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
describe("ConflictCandidateStore", () => {
|
|
128
|
+
it("saves, lists, and fetches open conflicts by fingerprint", async () => {
|
|
129
|
+
const store = createTempConflictStore();
|
|
130
|
+
const record = createConflictRecord();
|
|
131
|
+
await store.save(record);
|
|
132
|
+
|
|
133
|
+
const fetched = await store.getById(record.conflictId);
|
|
134
|
+
expect(fetched?.conflictId).toBe(record.conflictId);
|
|
135
|
+
|
|
136
|
+
const byFingerprint = await store.getOpenByFingerprint(record.fingerprint);
|
|
137
|
+
expect(byFingerprint?.conflictId).toBe(record.conflictId);
|
|
138
|
+
|
|
139
|
+
const recent = await store.listRecent({
|
|
140
|
+
status: "open",
|
|
141
|
+
canonicalKey: "user-reply-style",
|
|
142
|
+
limit: 10,
|
|
143
|
+
});
|
|
144
|
+
expect(recent).toHaveLength(1);
|
|
145
|
+
expect(recent[0]?.conflictId).toBe(record.conflictId);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("accepts a unique short conflict ID prefix", async () => {
|
|
149
|
+
const store = createTempConflictStore();
|
|
150
|
+
const record = createConflictRecord();
|
|
151
|
+
await store.save(record);
|
|
152
|
+
|
|
153
|
+
const fetched = await store.getById(record.conflictId.slice(0, 8));
|
|
154
|
+
expect(fetched?.conflictId).toBe(record.conflictId);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("resolveConflictCandidate", () => {
|
|
159
|
+
it("accepts incoming text and updates the existing durable memory", async () => {
|
|
160
|
+
const conflictStore = createTempConflictStore();
|
|
161
|
+
const record = createConflictRecord();
|
|
162
|
+
await conflictStore.save(record);
|
|
163
|
+
|
|
164
|
+
const updatedPayloads: any[] = [];
|
|
165
|
+
const existingMemory = {
|
|
166
|
+
id: "durable-1",
|
|
167
|
+
text: "User prefers concise, direct replies.",
|
|
168
|
+
vector: [1, 1, 1],
|
|
169
|
+
category: "preferences",
|
|
170
|
+
scope: "memory:agent",
|
|
171
|
+
importance: 0.84,
|
|
172
|
+
timestamp: 1_700_000_000_001,
|
|
173
|
+
metadata: record.existing.metadata,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const result = await resolveConflictCandidate({
|
|
177
|
+
store: {
|
|
178
|
+
async getById(id: string) {
|
|
179
|
+
return id === existingMemory.id ? existingMemory : null;
|
|
180
|
+
},
|
|
181
|
+
async update(id: string, updates: any) {
|
|
182
|
+
updatedPayloads.push({ id, updates });
|
|
183
|
+
Object.assign(existingMemory, updates);
|
|
184
|
+
return existingMemory;
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
embedder: {
|
|
188
|
+
async embedPassage(text: string) {
|
|
189
|
+
return [text.length, 9, 9];
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
conflictStore,
|
|
193
|
+
}, {
|
|
194
|
+
conflictId: record.conflictId,
|
|
195
|
+
resolution: "accept_incoming",
|
|
196
|
+
notes: "Promoted evidence is cleaner than the original durable wording.",
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
expect(result.status).toBe("accepted-incoming");
|
|
200
|
+
expect(result.updatedMemoryId).toBe("durable-1");
|
|
201
|
+
expect(updatedPayloads).toHaveLength(1);
|
|
202
|
+
expect(updatedPayloads[0]?.updates.text).toBe(record.incoming.text);
|
|
203
|
+
expect(updatedPayloads[0]?.updates.metadata).toBe(record.incoming.metadata);
|
|
204
|
+
|
|
205
|
+
const resolved = await conflictStore.getById(record.conflictId);
|
|
206
|
+
expect(resolved?.status).toBe("accepted-incoming");
|
|
207
|
+
expect(resolved?.resolutionNotes).toContain("cleaner");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("can keep the existing durable memory without updating it", async () => {
|
|
211
|
+
const conflictStore = createTempConflictStore();
|
|
212
|
+
const record = createConflictRecord();
|
|
213
|
+
await conflictStore.save(record);
|
|
214
|
+
|
|
215
|
+
let updated = false;
|
|
216
|
+
const result = await resolveConflictCandidate({
|
|
217
|
+
store: {
|
|
218
|
+
async getById() {
|
|
219
|
+
return null;
|
|
220
|
+
},
|
|
221
|
+
async update() {
|
|
222
|
+
updated = true;
|
|
223
|
+
return null;
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
embedder: {
|
|
227
|
+
async embedPassage() {
|
|
228
|
+
return [0, 0, 0];
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
conflictStore,
|
|
232
|
+
}, {
|
|
233
|
+
conflictId: record.conflictId,
|
|
234
|
+
resolution: "keep_existing",
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
expect(result.status).toBe("kept-existing");
|
|
238
|
+
expect(updated).toBe(false);
|
|
239
|
+
|
|
240
|
+
const resolved = await conflictStore.getById(record.conflictId);
|
|
241
|
+
expect(resolved?.status).toBe("kept-existing");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("merges same-category conflicts using the default merge suggestion", async () => {
|
|
245
|
+
const conflictStore = createTempConflictStore();
|
|
246
|
+
const record = createMergeableConflictRecord();
|
|
247
|
+
await conflictStore.save(record);
|
|
248
|
+
|
|
249
|
+
const updatedPayloads: any[] = [];
|
|
250
|
+
const existingMemory = {
|
|
251
|
+
id: "durable-1",
|
|
252
|
+
text: record.existing.text,
|
|
253
|
+
vector: [1, 1, 1],
|
|
254
|
+
category: "preferences",
|
|
255
|
+
scope: "memory:agent",
|
|
256
|
+
importance: 0.84,
|
|
257
|
+
timestamp: 1_700_000_000_001,
|
|
258
|
+
metadata: record.existing.metadata,
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const mergeSuggestion = summarizeConflictAdvice(record).mergeSuggestion;
|
|
262
|
+
expect(mergeSuggestion).toBeTruthy();
|
|
263
|
+
|
|
264
|
+
const result = await resolveConflictCandidate({
|
|
265
|
+
store: {
|
|
266
|
+
async getById(id: string) {
|
|
267
|
+
return id === existingMemory.id ? existingMemory : null;
|
|
268
|
+
},
|
|
269
|
+
async update(id: string, updates: any) {
|
|
270
|
+
updatedPayloads.push({ id, updates });
|
|
271
|
+
Object.assign(existingMemory, updates);
|
|
272
|
+
return existingMemory;
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
embedder: {
|
|
276
|
+
async embedPassage(text: string) {
|
|
277
|
+
return [text.length, 7, 7];
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
conflictStore,
|
|
281
|
+
}, {
|
|
282
|
+
conflictId: record.conflictId,
|
|
283
|
+
resolution: "merge",
|
|
284
|
+
notes: "Combine the grounded style and tone guidance.",
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
expect(result.status).toBe("merged");
|
|
288
|
+
expect(result.updatedMemoryId).toBe("durable-1");
|
|
289
|
+
expect(updatedPayloads).toHaveLength(1);
|
|
290
|
+
expect(updatedPayloads[0]?.updates.text).toBe(mergeSuggestion);
|
|
291
|
+
expect(updatedPayloads[0]?.updates.importance).toBe(0.84);
|
|
292
|
+
|
|
293
|
+
const mergedMetadata = JSON.parse(updatedPayloads[0]?.updates.metadata || "{}");
|
|
294
|
+
expect(mergedMetadata.canonicalKey).toBe("user-reply-style");
|
|
295
|
+
expect(mergedMetadata.promotedFrom?.memoryId).toBe("source-1");
|
|
296
|
+
expect(mergedMetadata.mergedFrom?.conflictId).toBe(record.conflictId);
|
|
297
|
+
|
|
298
|
+
const resolved = await conflictStore.getById(record.conflictId);
|
|
299
|
+
expect(resolved?.status).toBe("merged");
|
|
300
|
+
expect(resolved?.resolutionNotes).toContain("grounded style");
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("rejects merge for cross-category conflicts", async () => {
|
|
304
|
+
const conflictStore = createTempConflictStore();
|
|
305
|
+
const record = buildConflictCandidateRecord({
|
|
306
|
+
canonicalKey: "project-owner",
|
|
307
|
+
category: "events",
|
|
308
|
+
fingerprint: "project-owner--durable-1--source-2--cross-category",
|
|
309
|
+
reason: "canonical_key_conflicts_with_existing_durable",
|
|
310
|
+
existing: {
|
|
311
|
+
memoryId: "durable-1",
|
|
312
|
+
text: "Project owner: RecallNest",
|
|
313
|
+
category: "entities",
|
|
314
|
+
scope: "memory:agent",
|
|
315
|
+
importance: 0.82,
|
|
316
|
+
metadata: recordLikeMetadata("project-owner", "entities"),
|
|
317
|
+
canonicalKey: "project-owner",
|
|
318
|
+
},
|
|
319
|
+
incoming: {
|
|
320
|
+
text: "Project owner observation imported as an event.",
|
|
321
|
+
category: "events",
|
|
322
|
+
scope: "memory:agent",
|
|
323
|
+
importance: 0.74,
|
|
324
|
+
metadata: recordLikeMetadata("project-owner", "events"),
|
|
325
|
+
source: "agent",
|
|
326
|
+
sourceMemoryId: "source-2",
|
|
327
|
+
sourceCategory: "events",
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
await conflictStore.save(record);
|
|
331
|
+
|
|
332
|
+
await expect(resolveConflictCandidate({
|
|
333
|
+
store: {
|
|
334
|
+
async getById() {
|
|
335
|
+
return {
|
|
336
|
+
id: "durable-1",
|
|
337
|
+
text: "Project owner: RecallNest",
|
|
338
|
+
vector: [1, 1, 1],
|
|
339
|
+
category: "entities",
|
|
340
|
+
scope: "memory:agent",
|
|
341
|
+
importance: 0.82,
|
|
342
|
+
timestamp: 1_700_000_000_002,
|
|
343
|
+
metadata: record.existing.metadata,
|
|
344
|
+
};
|
|
345
|
+
},
|
|
346
|
+
async update() {
|
|
347
|
+
throw new Error("update should not be called");
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
embedder: {
|
|
351
|
+
async embedPassage() {
|
|
352
|
+
return [0, 0, 0];
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
conflictStore,
|
|
356
|
+
}, {
|
|
357
|
+
conflictId: record.conflictId,
|
|
358
|
+
resolution: "merge",
|
|
359
|
+
})).rejects.toThrow("same-category durable conflicts");
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
function recordLikeMetadata(canonicalKey: string, category: string): string {
|
|
364
|
+
return JSON.stringify({
|
|
365
|
+
source: "agent",
|
|
366
|
+
canonicalKey,
|
|
367
|
+
boundary: {
|
|
368
|
+
layer: "durable",
|
|
369
|
+
authority: "structured-memory",
|
|
370
|
+
conflictPolicy: "latest-wins",
|
|
371
|
+
originalCategory: category,
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { ConsolidationEngine, DEFAULT_CONSOLIDATION_CONFIG, formatConsolidationResult, type ConsolidationResult } from "../consolidation-engine.js";
|
|
4
|
+
import type { MemoryEntry, MemorySearchResult } from "../store.js";
|
|
5
|
+
|
|
6
|
+
function makeEntry(overrides: Partial<MemoryEntry> & { id: string; text: string }): MemoryEntry {
|
|
7
|
+
return {
|
|
8
|
+
vector: [1, 0, 0],
|
|
9
|
+
category: "events",
|
|
10
|
+
scope: "project:test",
|
|
11
|
+
importance: 0.7,
|
|
12
|
+
timestamp: Date.now(),
|
|
13
|
+
metadata: "{}",
|
|
14
|
+
...overrides,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function createMockStore(entries: MemoryEntry[], similarityMap: Map<string, Map<string, number>> = new Map()) {
|
|
19
|
+
const data = new Map(entries.map(e => [e.id, { ...e }]));
|
|
20
|
+
const updates: Array<{ id: string; metadata: string }> = [];
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
updates,
|
|
24
|
+
store: {
|
|
25
|
+
async list(scopeFilter?: string[], _category?: string, limit = 500, _offset = 0) {
|
|
26
|
+
return [...data.values()]
|
|
27
|
+
.filter(e => !scopeFilter || scopeFilter.some(s => e.scope === s))
|
|
28
|
+
.slice(0, limit);
|
|
29
|
+
},
|
|
30
|
+
async getById(id: string) {
|
|
31
|
+
return data.get(id) ?? null;
|
|
32
|
+
},
|
|
33
|
+
async vectorSearch(vector: number[], limit = 5, minScore = 0.3, scopeFilter?: string[]) {
|
|
34
|
+
// Use the similarity map to compute fake scores
|
|
35
|
+
const sourceEntry = [...data.values()].find(e =>
|
|
36
|
+
e.vector.length === vector.length && e.vector.every((v, i) => v === vector[i])
|
|
37
|
+
);
|
|
38
|
+
if (!sourceEntry) return [];
|
|
39
|
+
|
|
40
|
+
const sourceMap = similarityMap.get(sourceEntry.id);
|
|
41
|
+
if (!sourceMap) return [];
|
|
42
|
+
|
|
43
|
+
const results: MemorySearchResult[] = [];
|
|
44
|
+
for (const [targetId, score] of sourceMap) {
|
|
45
|
+
if (score < minScore) continue;
|
|
46
|
+
const target = data.get(targetId);
|
|
47
|
+
if (!target) continue;
|
|
48
|
+
if (scopeFilter && !scopeFilter.some(s => target.scope === s)) continue;
|
|
49
|
+
results.push({ entry: target, score });
|
|
50
|
+
}
|
|
51
|
+
return results.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
52
|
+
},
|
|
53
|
+
async update(id: string, upd: { metadata?: string }) {
|
|
54
|
+
const entry = data.get(id);
|
|
55
|
+
if (!entry) return null;
|
|
56
|
+
if (upd.metadata) {
|
|
57
|
+
entry.metadata = upd.metadata;
|
|
58
|
+
updates.push({ id, metadata: upd.metadata });
|
|
59
|
+
}
|
|
60
|
+
return entry;
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe("ConsolidationEngine", () => {
|
|
67
|
+
it("returns empty result for empty scope", async () => {
|
|
68
|
+
const { store } = createMockStore([]);
|
|
69
|
+
const engine = new ConsolidationEngine(store);
|
|
70
|
+
const result = await engine.run("project:test");
|
|
71
|
+
expect(result.originalCount).toBe(0);
|
|
72
|
+
expect(result.clustersFound).toBe(0);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("skips single-entry categories", async () => {
|
|
76
|
+
const entries = [makeEntry({ id: "a", text: "only one" })];
|
|
77
|
+
const { store } = createMockStore(entries);
|
|
78
|
+
const engine = new ConsolidationEngine(store);
|
|
79
|
+
const result = await engine.run("project:test");
|
|
80
|
+
expect(result.originalCount).toBe(1);
|
|
81
|
+
expect(result.clustersFound).toBe(0);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("merges near-duplicates above mergeThreshold", async () => {
|
|
85
|
+
const entryA = makeEntry({ id: "a", text: "I prefer TypeScript", vector: [1, 0, 0], importance: 0.9 });
|
|
86
|
+
const entryB = makeEntry({ id: "b", text: "I prefer TypeScript language", vector: [0.99, 0.1, 0], importance: 0.5 });
|
|
87
|
+
|
|
88
|
+
const simMap = new Map([
|
|
89
|
+
["a", new Map([["b", 0.95]])],
|
|
90
|
+
["b", new Map([["a", 0.95]])],
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
const { store, updates } = createMockStore([entryA, entryB], simMap);
|
|
94
|
+
const engine = new ConsolidationEngine(store, { ...DEFAULT_CONSOLIDATION_CONFIG, mergeThreshold: 0.92 });
|
|
95
|
+
const result = await engine.run("project:test");
|
|
96
|
+
|
|
97
|
+
expect(result.clustersFound).toBe(1);
|
|
98
|
+
expect(result.mergedCount).toBe(1);
|
|
99
|
+
// Tier 3.3: Both entries now coexist in a version group instead of archiving.
|
|
100
|
+
// Both A and B should have version_group metadata.
|
|
101
|
+
const updateA = updates.find(u => u.id === "a");
|
|
102
|
+
const updateB = updates.find(u => u.id === "b");
|
|
103
|
+
expect(updateA).toBeTruthy();
|
|
104
|
+
expect(updateB).toBeTruthy();
|
|
105
|
+
const metaA = JSON.parse(updateA!.metadata);
|
|
106
|
+
const metaB = JSON.parse(updateB!.metadata);
|
|
107
|
+
expect(metaA.version_group).toBeTruthy();
|
|
108
|
+
expect(metaB.version_group).toBe(metaA.version_group);
|
|
109
|
+
// Canonical (A, higher importance) should have higher rank
|
|
110
|
+
expect(metaA.version_rank).toBeGreaterThan(metaB.version_rank);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("links related entries below mergeThreshold but above clusterThreshold", async () => {
|
|
114
|
+
const entryA = makeEntry({ id: "a", text: "TypeScript config", vector: [1, 0, 0] });
|
|
115
|
+
const entryB = makeEntry({ id: "b", text: "TypeScript setup", vector: [0.9, 0.1, 0] });
|
|
116
|
+
|
|
117
|
+
const simMap = new Map([
|
|
118
|
+
["a", new Map([["b", 0.85]])],
|
|
119
|
+
["b", new Map([["a", 0.85]])],
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
const { store, updates } = createMockStore([entryA, entryB], simMap);
|
|
123
|
+
const engine = new ConsolidationEngine(store, { ...DEFAULT_CONSOLIDATION_CONFIG, clusterThreshold: 0.82, mergeThreshold: 0.92 });
|
|
124
|
+
const result = await engine.run("project:test");
|
|
125
|
+
|
|
126
|
+
expect(result.clustersFound).toBe(1);
|
|
127
|
+
expect(result.mergedCount).toBe(0);
|
|
128
|
+
expect(result.relationsAdded).toBe(1);
|
|
129
|
+
// Both should have clustering metadata
|
|
130
|
+
const linkUpdate = updates.find(u => u.id === "b");
|
|
131
|
+
expect(linkUpdate).toBeTruthy();
|
|
132
|
+
const meta = JSON.parse(linkUpdate!.metadata);
|
|
133
|
+
expect(meta.clustered_with).toBe("a");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("detects heuristic contradictions", async () => {
|
|
137
|
+
const entryA = makeEntry({ id: "a", text: "Always use strict mode in TypeScript projects", vector: [1, 0, 0] });
|
|
138
|
+
const entryB = makeEntry({ id: "b", text: "Never use strict mode in TypeScript projects", vector: [0.98, 0.1, 0] });
|
|
139
|
+
|
|
140
|
+
const simMap = new Map([
|
|
141
|
+
["a", new Map([["b", 0.95]])],
|
|
142
|
+
["b", new Map([["a", 0.95]])],
|
|
143
|
+
]);
|
|
144
|
+
|
|
145
|
+
const { store } = createMockStore([entryA, entryB], simMap);
|
|
146
|
+
const engine = new ConsolidationEngine(store, DEFAULT_CONSOLIDATION_CONFIG);
|
|
147
|
+
const result = await engine.run("project:test");
|
|
148
|
+
|
|
149
|
+
expect(result.conflictsDetected.length).toBe(1);
|
|
150
|
+
expect(result.conflictsDetected[0].type).toBe("heuristic_contradiction");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("skips archived entries", async () => {
|
|
154
|
+
const entryA = makeEntry({ id: "a", text: "active entry here", vector: [1, 0, 0] });
|
|
155
|
+
const entryB = makeEntry({ id: "b", text: "archived entry here", vector: [0.95, 0.1, 0], metadata: JSON.stringify({ state: "archived" }) });
|
|
156
|
+
|
|
157
|
+
const { store } = createMockStore([entryA, entryB]);
|
|
158
|
+
const engine = new ConsolidationEngine(store);
|
|
159
|
+
const result = await engine.run("project:test");
|
|
160
|
+
|
|
161
|
+
expect(result.originalCount).toBe(1); // only active
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("formatConsolidationResult", () => {
|
|
166
|
+
it("formats a result with conflicts", () => {
|
|
167
|
+
const result: ConsolidationResult = {
|
|
168
|
+
originalCount: 100,
|
|
169
|
+
clustersFound: 5,
|
|
170
|
+
mergedCount: 3,
|
|
171
|
+
relationsAdded: 7,
|
|
172
|
+
conflictsDetected: [{ memoryA: "aaaa-bbbb", memoryB: "cccc-dddd", type: "heuristic_contradiction" }],
|
|
173
|
+
scope: "project:test",
|
|
174
|
+
};
|
|
175
|
+
const text = formatConsolidationResult(result);
|
|
176
|
+
expect(text).toContain("Scanned: 100");
|
|
177
|
+
expect(text).toContain("Clusters found: 5");
|
|
178
|
+
expect(text).toContain("Merged (versioned): 3");
|
|
179
|
+
expect(text).toContain("Conflicts:");
|
|
180
|
+
});
|
|
181
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import type { PinAsset } from "../memory-assets.js";
|
|
4
|
+
import { selectPinnedContext } from "../context-composer-pins.js";
|
|
5
|
+
|
|
6
|
+
function buildPinnedAsset(overrides: Partial<PinAsset> = {}): PinAsset & { path: string } {
|
|
7
|
+
return {
|
|
8
|
+
id: overrides.id || "pin-1",
|
|
9
|
+
type: "pinned-memory",
|
|
10
|
+
createdAt: overrides.createdAt || "2026-03-20T00:00:00.000Z",
|
|
11
|
+
updatedAt: overrides.updatedAt || "2026-03-20T00:00:00.000Z",
|
|
12
|
+
title: overrides.title || "Pinned continuity note",
|
|
13
|
+
summary: overrides.summary || "Pinned continuity reminder",
|
|
14
|
+
tags: overrides.tags || [],
|
|
15
|
+
source: overrides.source || {
|
|
16
|
+
memoryId: "memory-1",
|
|
17
|
+
scope: "memory:project:recallnest",
|
|
18
|
+
timestamp: Date.parse("2026-03-20T00:00:00.000Z"),
|
|
19
|
+
metadata: {},
|
|
20
|
+
},
|
|
21
|
+
retrieval: overrides.retrieval,
|
|
22
|
+
snippet: overrides.snippet || "",
|
|
23
|
+
path: "/tmp/pin-1.json",
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("context composer pins", () => {
|
|
28
|
+
it("keeps later task-specific cues from long maintenance prompts", () => {
|
|
29
|
+
const taskSeed =
|
|
30
|
+
"继续 RecallNest continuity helper boundary audit ranking scoring selection orchestration context composer stable query fallback profile forwarding gap runner isolation";
|
|
31
|
+
|
|
32
|
+
const pinnedContext = selectPinnedContext([
|
|
33
|
+
buildPinnedAsset({
|
|
34
|
+
title: "Eval runner isolation note",
|
|
35
|
+
summary: "Fresh-window replay still needs explicit runner isolation to avoid shared component skew.",
|
|
36
|
+
tags: ["recallnest", "runner", "isolation"],
|
|
37
|
+
snippet: "Runner isolation is the key safeguard when continuity previews diverge across fresh-window replay.",
|
|
38
|
+
}),
|
|
39
|
+
], {
|
|
40
|
+
taskSeed,
|
|
41
|
+
limit: 1,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
expect(pinnedContext).toHaveLength(1);
|
|
45
|
+
expect(pinnedContext[0]).toContain("Pinned: Eval runner isolation note: Fresh-window replay still needs explicit runner isolation to avoid shared component skew.");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("ignores conversational continuation filler for vague memory-layer prompts", () => {
|
|
49
|
+
const pinnedContext = selectPinnedContext([
|
|
50
|
+
buildPinnedAsset({
|
|
51
|
+
id: "pin-visual",
|
|
52
|
+
title: "用户视觉审美偏好",
|
|
53
|
+
summary: "用户常用视觉风格是手绘涂鸦风加高对比撞色;在内容包装和配图生成时,应优先沿用这一审美方向。",
|
|
54
|
+
tags: ["审美偏好", "手绘涂鸦", "高对比撞色", "给刚才写的文章生成配图"],
|
|
55
|
+
snippet: "给刚才写的文章生成配图,风格:手绘涂鸦风+高对比撞色。",
|
|
56
|
+
}),
|
|
57
|
+
], {
|
|
58
|
+
taskSeed: "把刚才那个 memory layer 接回去",
|
|
59
|
+
limit: 1,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(pinnedContext).toEqual([]);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { countTaskSpecificTextMatches } from "../context-composer-task-selection.js";
|
|
4
|
+
|
|
5
|
+
describe("context composer task selection", () => {
|
|
6
|
+
it("keeps later task-specific cues from long maintenance prompts", () => {
|
|
7
|
+
const taskSeed =
|
|
8
|
+
"继续 RecallNest continuity helper boundary audit ranking scoring selection orchestration context composer stable query fallback profile forwarding gap runner isolation";
|
|
9
|
+
|
|
10
|
+
expect(
|
|
11
|
+
countTaskSpecificTextMatches(
|
|
12
|
+
"Case: Continuity eval profile forwarding gap Problem: sparse writing prompts still failed until the eval profile was forwarded correctly.",
|
|
13
|
+
taskSeed,
|
|
14
|
+
),
|
|
15
|
+
).toBeGreaterThan(0);
|
|
16
|
+
|
|
17
|
+
expect(
|
|
18
|
+
countTaskSpecificTextMatches(
|
|
19
|
+
"Case: Eval runner shared components skewed later continuity previews Problem: runner isolation broke when shared component state leaked across fresh-window replay.",
|
|
20
|
+
taskSeed,
|
|
21
|
+
),
|
|
22
|
+
).toBeGreaterThan(0);
|
|
23
|
+
});
|
|
24
|
+
});
|