recallnest 2.6.0 → 3.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 +191 -0
- package/README.md +131 -14
- package/README_CN.md +121 -17
- package/config.json.example +8 -1
- package/docs/api-reference.md +1 -1
- package/integrations/README.md +1 -1
- package/integrations/agy/README.md +73 -0
- package/integrations/agy/setup.sh +75 -0
- package/integrations/setup-all.sh +1 -1
- package/package.json +7 -5
- package/src/__tests__/access-tracker-novelty.test.ts +22 -2
- package/src/__tests__/activity-counter.test.ts +38 -38
- package/src/__tests__/age-label.test.ts +33 -0
- package/src/__tests__/cluster-consolidation.test.ts +313 -22
- package/src/__tests__/consolidation-engine.test.ts +90 -0
- package/src/__tests__/contracts.test.ts +2 -2
- package/src/__tests__/data-checkup.test.ts +31 -3
- package/src/__tests__/dream-pipeline.test.ts +41 -1
- package/src/__tests__/dream-scope-isolation.test.ts +386 -0
- package/src/__tests__/dream-write-batching.test.ts +211 -0
- package/src/__tests__/env-config-invariant.test.ts +80 -0
- package/src/__tests__/env-config.test.ts +60 -0
- package/src/__tests__/ingest-scope-prefix.test.ts +69 -0
- package/src/__tests__/maintenance-locks.test.ts +8 -0
- package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +17 -0
- package/src/__tests__/memory-output.test.ts +9 -0
- package/src/__tests__/memory-utility.test.ts +256 -0
- package/src/__tests__/openai-contract.test.ts +513 -0
- package/src/__tests__/promotion-distinct-sources.test.ts +162 -0
- package/src/__tests__/retrieve-audit-revision.test.ts +255 -0
- package/src/__tests__/source-heartbeat.test.ts +6 -2
- package/src/__tests__/store-patch-metadata.test.ts +30 -0
- package/src/__tests__/synthesis-contract.test.ts +252 -0
- package/src/__tests__/synthesis-promotion.test.ts +369 -0
- package/src/__tests__/trusted-memory.test.ts +28 -0
- package/src/__tests__/utility-weight.test.ts +57 -0
- package/src/__tests__/verbatim-self-recall.test.ts +279 -0
- package/src/__tests__/version-manager.test.ts +35 -0
- package/src/access-tracker.ts +10 -1
- package/src/activity-counter.ts +69 -11
- package/src/age-label.ts +29 -0
- package/src/audit-log.ts +35 -0
- package/src/auto-gc.ts +97 -30
- package/src/cli.ts +224 -10
- package/src/consolidation-engine.ts +317 -123
- package/src/data-checkup.ts +17 -3
- package/src/dream-pipeline.ts +90 -12
- package/src/embedder.ts +20 -5
- package/src/env-config.ts +53 -0
- package/src/forget-engine.ts +8 -2
- package/src/gateway-server.ts +272 -0
- package/src/ingest.ts +23 -5
- package/src/llm-client.ts +86 -25
- package/src/mcp-server.ts +2 -1
- package/src/mcp-tools-advanced.ts +7 -2
- package/src/mcp-tools-core.ts +11 -1
- package/src/mcp-tools-governance.ts +45 -3
- package/src/memory-evolution.ts +16 -0
- package/src/memory-output.ts +12 -8
- package/src/memory-promotion.ts +280 -0
- package/src/memory-store-port.ts +32 -3
- package/src/memory-utility.ts +314 -0
- package/src/recall-ledger.ts +76 -0
- package/src/retriever.ts +82 -9
- package/src/scope-policy.ts +22 -1
- package/src/session-output.ts +4 -3
- package/src/skill-promotion.ts +152 -1
- package/src/store.ts +231 -71
- package/src/synthesis-contract.ts +286 -0
- package/src/version-manager.ts +83 -0
- package/src/workflow-observation-schema.ts +26 -0
- package/integrations/gemini-cli/README.md +0 -57
- package/integrations/gemini-cli/setup.sh +0 -64
- /package/integrations/{gemini-cli/gemini-md-snippet.md → agy/agy-md-snippet.md} +0 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { describe, expect, it, beforeEach } from "bun:test";
|
|
2
|
+
import { mkdtempSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
applyUtilityToMeta,
|
|
8
|
+
computeMemoryUtility,
|
|
9
|
+
formatMemoryUtility,
|
|
10
|
+
JOIN_WEIGHT,
|
|
11
|
+
linkMemoryToObservations,
|
|
12
|
+
OUTCOME_REWARD,
|
|
13
|
+
readUtility,
|
|
14
|
+
summarizeMemoryOutcomes,
|
|
15
|
+
UTILITY_FIELD,
|
|
16
|
+
} from "../memory-utility.js";
|
|
17
|
+
import { buildWorkflowObservationRecord } from "../workflow-observation-engine.js";
|
|
18
|
+
import { WorkflowObservationStore } from "../workflow-observation-store.js";
|
|
19
|
+
import {
|
|
20
|
+
PROCESS_READER_ID,
|
|
21
|
+
RECALLED_IDS_CAP,
|
|
22
|
+
recallLedgerSize,
|
|
23
|
+
recentRecallHits,
|
|
24
|
+
recordRecallHits,
|
|
25
|
+
resetRecallLedger,
|
|
26
|
+
} from "../recall-ledger.js";
|
|
27
|
+
import { AccessTracker } from "../access-tracker.js";
|
|
28
|
+
import { isDecayExempt, resolveTier } from "../decay-engine.js";
|
|
29
|
+
|
|
30
|
+
const MEM_A = "aaaaaaaa-0000-4000-8000-000000000001";
|
|
31
|
+
const MEM_B = "bbbbbbbb-0000-4000-8000-000000000002";
|
|
32
|
+
|
|
33
|
+
function obs(overrides: Record<string, unknown>) {
|
|
34
|
+
return buildWorkflowObservationRecord({
|
|
35
|
+
workflowId: "resume_context",
|
|
36
|
+
summary: "test observation",
|
|
37
|
+
...overrides,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe("recall ledger (P0 join key)", () => {
|
|
42
|
+
beforeEach(() => resetRecallLedger());
|
|
43
|
+
|
|
44
|
+
it("records hits and returns them newest-first within the window", () => {
|
|
45
|
+
const t0 = 1_000_000;
|
|
46
|
+
recordRecallHits([MEM_A], t0);
|
|
47
|
+
recordRecallHits([MEM_B], t0 + 1000);
|
|
48
|
+
expect(recentRecallHits(60_000, 10, t0 + 2000)).toEqual([MEM_B, MEM_A]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("drops hits older than the window", () => {
|
|
52
|
+
const t0 = 1_000_000;
|
|
53
|
+
recordRecallHits([MEM_A], t0);
|
|
54
|
+
recordRecallHits([MEM_B], t0 + 60 * 60_000);
|
|
55
|
+
expect(recentRecallHits(30 * 60_000, 10, t0 + 60 * 60_000)).toEqual([MEM_B]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("caps the returned list", () => {
|
|
59
|
+
const ids = Array.from({ length: RECALLED_IDS_CAP + 20 }, (_, i) => `id-${i}`);
|
|
60
|
+
recordRecallHits(ids, 5_000);
|
|
61
|
+
expect(recentRecallHits(60_000, RECALLED_IDS_CAP, 5_000).length).toBe(RECALLED_IDS_CAP);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("gives every AccessTracker the same process-level reader id", () => {
|
|
65
|
+
// 回归防线:readerId 曾是 per-instance 随机的,而 components 按 profile 缓存 —— 同一个
|
|
66
|
+
// 会话开两个 profile 就伪造出两个 reader,join 直接失效。
|
|
67
|
+
const fakeStore = {} as never;
|
|
68
|
+
expect(new AccessTracker(fakeStore).readerId).toBe(PROCESS_READER_ID);
|
|
69
|
+
expect(new AccessTracker(fakeStore).readerId).toBe(new AccessTracker(fakeStore).readerId);
|
|
70
|
+
expect(new AccessTracker(fakeStore, undefined, "r-explicit").readerId).toBe("r-explicit");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("records into the ledger before the novelty/cooldown gate", () => {
|
|
74
|
+
// 高相似度 + 严格 novelty 闸:强化会被挡下,但 agent 确实看到了这条 —— 账本必须记。
|
|
75
|
+
const fakeStore = {} as never;
|
|
76
|
+
const tracker = new AccessTracker(fakeStore, {
|
|
77
|
+
flushIntervalMs: 5000,
|
|
78
|
+
reinforcementFactor: 0.5,
|
|
79
|
+
maxMultiplier: 3,
|
|
80
|
+
accessFreshnessHalfLifeDays: 30,
|
|
81
|
+
noveltyThreshold: 0.9,
|
|
82
|
+
cooldownMs: 0,
|
|
83
|
+
});
|
|
84
|
+
tracker.recordAccess([MEM_A], [0.99]);
|
|
85
|
+
expect(tracker.pendingCount).toBe(0);
|
|
86
|
+
expect(recentRecallHits()).toContain(MEM_A);
|
|
87
|
+
tracker.destroy();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("memory ↔ observation join", () => {
|
|
92
|
+
it("joins exactly when the observation names the memory", () => {
|
|
93
|
+
const links = linkMemoryToObservations(MEM_A, "{}", [
|
|
94
|
+
obs({ recalledIds: [MEM_A, MEM_B], outcome: "success" }),
|
|
95
|
+
obs({ recalledIds: [MEM_B], outcome: "failure" }),
|
|
96
|
+
]);
|
|
97
|
+
expect(links.length).toBe(1);
|
|
98
|
+
expect(links[0].join).toBe("exact");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("falls back to session join through readerId ∩ metadata.readerIds", () => {
|
|
102
|
+
const meta = JSON.stringify({ readerIds: ["r-1234abcd"] });
|
|
103
|
+
const links = linkMemoryToObservations(MEM_A, meta, [
|
|
104
|
+
obs({ readerId: "r-1234abcd", outcome: "corrected" }),
|
|
105
|
+
obs({ readerId: "r-otherxx", outcome: "failure" }),
|
|
106
|
+
]);
|
|
107
|
+
expect(links.length).toBe(1);
|
|
108
|
+
expect(links[0].join).toBe("session");
|
|
109
|
+
expect(links[0].outcome).toBe("corrected");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("prefers exact over session for the same observation", () => {
|
|
113
|
+
const meta = JSON.stringify({ readerIds: ["r-1234abcd"] });
|
|
114
|
+
const links = linkMemoryToObservations(MEM_A, meta, [
|
|
115
|
+
obs({ readerId: "r-1234abcd", recalledIds: [MEM_A], outcome: "success" }),
|
|
116
|
+
]);
|
|
117
|
+
expect(links.length).toBe(1);
|
|
118
|
+
expect(links[0].join).toBe("exact");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("returns nothing when neither key is present (the pre-P0 state)", () => {
|
|
122
|
+
expect(linkMemoryToObservations(MEM_A, "{}", [obs({ outcome: "success" })])).toEqual([]);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("utility computation", () => {
|
|
127
|
+
const now = Date.parse("2026-08-23T00:00:00.000Z");
|
|
128
|
+
const at = (daysAgo: number) => new Date(now - daysAgo * 86_400_000).toISOString();
|
|
129
|
+
|
|
130
|
+
it("answers 「这条 memory 参与过的任务成功率是多少」", () => {
|
|
131
|
+
const summary = summarizeMemoryOutcomes(MEM_A, [
|
|
132
|
+
{ observationId: "o1", workflowId: "w", outcome: "success", recordedAt: at(1), join: "exact", summary: "" },
|
|
133
|
+
{ observationId: "o2", workflowId: "w", outcome: "success", recordedAt: at(2), join: "exact", summary: "" },
|
|
134
|
+
{ observationId: "o3", workflowId: "w", outcome: "failure", recordedAt: at(3), join: "exact", summary: "" },
|
|
135
|
+
], now);
|
|
136
|
+
expect(summary.total).toBe(3);
|
|
137
|
+
expect(summary.successRate).toBeCloseTo(2 / 3, 5);
|
|
138
|
+
expect(summary.exactSuccessRate).toBeCloseTo(2 / 3, 5);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("goes negative when the tasks mostly failed — importance could never hold this", () => {
|
|
142
|
+
const summary = summarizeMemoryOutcomes(MEM_A, [
|
|
143
|
+
{ observationId: "o1", workflowId: "w", outcome: "failure", recordedAt: at(0), join: "exact", summary: "" },
|
|
144
|
+
{ observationId: "o2", workflowId: "w", outcome: "failure", recordedAt: at(1), join: "exact", summary: "" },
|
|
145
|
+
{ observationId: "o3", workflowId: "w", outcome: "missed", recordedAt: at(2), join: "exact", summary: "" },
|
|
146
|
+
], now);
|
|
147
|
+
expect(summary.utility).not.toBeNull();
|
|
148
|
+
expect(summary.utility!).toBeLessThan(0);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("weights session joins below exact ones", () => {
|
|
152
|
+
const exact = summarizeMemoryOutcomes(MEM_A, [
|
|
153
|
+
{ observationId: "o1", workflowId: "w", outcome: "success", recordedAt: at(0), join: "exact", summary: "" },
|
|
154
|
+
], now);
|
|
155
|
+
const session = summarizeMemoryOutcomes(MEM_A, [
|
|
156
|
+
{ observationId: "o1", workflowId: "w", outcome: "success", recordedAt: at(0), join: "session", summary: "" },
|
|
157
|
+
], now);
|
|
158
|
+
expect(exact.effectiveSamples).toBeCloseTo(JOIN_WEIGHT.exact, 5);
|
|
159
|
+
expect(session.effectiveSamples).toBeCloseTo(JOIN_WEIGHT.session, 5);
|
|
160
|
+
// 同一个 outcome,加权平均的值一样,可信度不同 —— 差别落在 effectiveSamples 上。
|
|
161
|
+
expect(exact.utility).toBeCloseTo(OUTCOME_REWARD.success, 5);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("decays old outcomes by the 30-day half-life", () => {
|
|
165
|
+
const fresh = summarizeMemoryOutcomes(MEM_A, [
|
|
166
|
+
{ observationId: "o1", workflowId: "w", outcome: "success", recordedAt: at(0), join: "exact", summary: "" },
|
|
167
|
+
], now);
|
|
168
|
+
const old = summarizeMemoryOutcomes(MEM_A, [
|
|
169
|
+
{ observationId: "o1", workflowId: "w", outcome: "success", recordedAt: at(30), join: "exact", summary: "" },
|
|
170
|
+
], now);
|
|
171
|
+
expect(old.effectiveSamples).toBeCloseTo(fresh.effectiveSamples * 0.5, 3);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("ignores session-join noise once exact samples exist", () => {
|
|
175
|
+
// 2026-08-23 落地当天的实测:同一会话跑多个任务时,readerId join 会把无关任务的
|
|
176
|
+
// 成败摊到每条被读过的记忆上——那次 5 条记忆的会话级成功率全是 44%(常数,零信息),
|
|
177
|
+
// 精确成功率却是 100%/33%/0%。所以有精确样本时不掺会话级。
|
|
178
|
+
const links = [
|
|
179
|
+
{ observationId: "e1", workflowId: "w", outcome: "success" as const, recordedAt: at(0), join: "exact" as const, summary: "" },
|
|
180
|
+
{ observationId: "e2", workflowId: "w", outcome: "success" as const, recordedAt: at(0), join: "exact" as const, summary: "" },
|
|
181
|
+
{ observationId: "s1", workflowId: "w", outcome: "failure" as const, recordedAt: at(0), join: "session" as const, summary: "" },
|
|
182
|
+
{ observationId: "s2", workflowId: "w", outcome: "failure" as const, recordedAt: at(0), join: "session" as const, summary: "" },
|
|
183
|
+
];
|
|
184
|
+
const preferred = summarizeMemoryOutcomes(MEM_A, links, now);
|
|
185
|
+
expect(preferred.utilityBasis).toBe("exact");
|
|
186
|
+
expect(preferred.utility).toBeCloseTo(OUTCOME_REWARD.success, 5);
|
|
187
|
+
|
|
188
|
+
const mixed = summarizeMemoryOutcomes(MEM_A, links, now, { preferExact: false });
|
|
189
|
+
expect(mixed.utility!).toBeLessThan(preferred.utility!);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("falls back to session join when nothing named this memory", () => {
|
|
193
|
+
const links = Array.from({ length: 4 }, (_, i) => ({
|
|
194
|
+
observationId: `s${i}`, workflowId: "w", outcome: "success" as const,
|
|
195
|
+
recordedAt: at(0), join: "session" as const, summary: "",
|
|
196
|
+
}));
|
|
197
|
+
const summary = summarizeMemoryOutcomes(MEM_A, links, now);
|
|
198
|
+
expect(summary.utilityBasis).toBe("session");
|
|
199
|
+
expect(summary.utility).toBeCloseTo(OUTCOME_REWARD.success, 5);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("returns null (not 0) when there is no evidence", () => {
|
|
203
|
+
const summary = summarizeMemoryOutcomes(MEM_A, [], now);
|
|
204
|
+
expect(summary.utility).toBeNull();
|
|
205
|
+
expect(summary.successRate).toBeNull();
|
|
206
|
+
expect(summary.utilityBasis).toBe("none");
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("utility metadata write-back", () => {
|
|
211
|
+
it("writes utility without touching importance-derived behaviour", () => {
|
|
212
|
+
const summary = summarizeMemoryOutcomes(MEM_A, [
|
|
213
|
+
{ observationId: "o1", workflowId: "w", outcome: "failure", recordedAt: new Date().toISOString(), join: "exact", summary: "" },
|
|
214
|
+
]);
|
|
215
|
+
const before = { tier: "peripheral", accessCount: 2 };
|
|
216
|
+
const after = applyUtilityToMeta(before, summary);
|
|
217
|
+
expect(after[UTILITY_FIELD]).toBeLessThan(0);
|
|
218
|
+
// 硬约束:utility 不进 tier、不进 decay 豁免。
|
|
219
|
+
expect(resolveTier(JSON.stringify(after), 0.7)).toBe(resolveTier(JSON.stringify(before), 0.7));
|
|
220
|
+
expect(isDecayExempt(JSON.stringify(after), 0.7, "cases"))
|
|
221
|
+
.toBe(isDecayExempt(JSON.stringify(before), 0.7, "cases"));
|
|
222
|
+
expect((after as Record<string, unknown>).importance).toBeUndefined();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("clears utility instead of writing 0 when the evidence is gone", () => {
|
|
226
|
+
const meta = { utility: -0.5, utilitySamples: 1, utilityUpdatedAt: 1 };
|
|
227
|
+
const cleared = applyUtilityToMeta(meta, summarizeMemoryOutcomes(MEM_A, []));
|
|
228
|
+
expect(UTILITY_FIELD in cleared).toBe(false);
|
|
229
|
+
expect(readUtility(JSON.stringify(cleared))).toBeNull();
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe("end-to-end through the observation store", () => {
|
|
234
|
+
it("round-trips join keys and answers the success-rate query", async () => {
|
|
235
|
+
const dir = mkdtempSync(join(tmpdir(), "recallnest-utility-obs-"));
|
|
236
|
+
const store = new WorkflowObservationStore(dir);
|
|
237
|
+
await store.save(obs({ recalledIds: [MEM_A], outcome: "success", readerId: "r-sess0001" }));
|
|
238
|
+
await store.save(obs({ recalledIds: [MEM_A], outcome: "failure", readerId: "r-sess0001", workflowId: "checkpoint_session" }));
|
|
239
|
+
await store.save(obs({ recalledIds: [MEM_B], outcome: "success", readerId: "r-sess0001" }));
|
|
240
|
+
|
|
241
|
+
const records = await store.listRecent({ limit: 100 });
|
|
242
|
+
const summary = computeMemoryUtility({ id: MEM_A, metadata: "{}" }, records);
|
|
243
|
+
expect(summary.total).toBe(2);
|
|
244
|
+
expect(summary.successRate).toBeCloseTo(0.5, 5);
|
|
245
|
+
expect(formatMemoryUtility(summary, { showLinks: 2 })).toContain("成功率");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("still parses observations written before the join-key fields existed", async () => {
|
|
249
|
+
const dir = mkdtempSync(join(tmpdir(), "recallnest-utility-legacy-"));
|
|
250
|
+
const store = new WorkflowObservationStore(dir);
|
|
251
|
+
const legacy = obs({ outcome: "success" });
|
|
252
|
+
expect((legacy as Record<string, unknown>).recalledIds).toBeUndefined();
|
|
253
|
+
await store.save(legacy);
|
|
254
|
+
expect((await store.listRecent({ limit: 10 })).length).toBe(1);
|
|
255
|
+
});
|
|
256
|
+
});
|