@clawmem-ai/clawmem 0.1.18 → 0.1.19
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/README.md +28 -9
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/src/collaboration.d.ts +49 -0
- package/dist/src/collaboration.js +69 -0
- package/dist/src/config.d.ts +21 -0
- package/dist/src/config.js +119 -0
- package/dist/src/conversation.d.ts +30 -0
- package/dist/src/conversation.js +323 -0
- package/dist/src/github-client.d.ts +269 -0
- package/dist/src/github-client.js +350 -0
- package/dist/src/keyed-async-queue.d.ts +12 -0
- package/dist/src/keyed-async-queue.js +23 -0
- package/dist/src/memory.d.ts +29 -0
- package/dist/src/memory.js +451 -0
- package/dist/src/recall-sanitize.d.ts +1 -0
- package/dist/src/recall-sanitize.js +149 -0
- package/dist/src/runtime-env.d.ts +2 -0
- package/dist/src/runtime-env.js +12 -0
- package/dist/src/service.d.ts +18 -0
- package/dist/src/service.js +3645 -0
- package/dist/src/state.d.ts +4 -0
- package/dist/src/state.js +182 -0
- package/dist/src/transcript.d.ts +3 -0
- package/dist/src/transcript.js +164 -0
- package/dist/src/types.d.ts +130 -0
- package/dist/src/types.js +1 -0
- package/dist/src/utils.d.ts +26 -0
- package/dist/src/utils.js +62 -0
- package/dist/src/yaml.d.ts +2 -0
- package/dist/src/yaml.js +81 -0
- package/openclaw.plugin.json +14 -1
- package/package.json +21 -7
- package/skills/clawmem/SKILL.md +26 -5
- package/skills/clawmem/references/collaboration.md +13 -5
- package/skills/clawmem/references/review.md +77 -0
- package/skills/clawmem/references/schema.md +44 -1
- package/index.ts +0 -6
- package/src/collaboration.test.ts +0 -71
- package/src/collaboration.ts +0 -109
- package/src/config.test.ts +0 -83
- package/src/config.ts +0 -117
- package/src/conversation.test.ts +0 -120
- package/src/conversation.ts +0 -304
- package/src/github-client.test.ts +0 -101
- package/src/github-client.ts +0 -363
- package/src/keyed-async-queue.ts +0 -26
- package/src/memory.test.ts +0 -588
- package/src/memory.ts +0 -444
- package/src/recall-sanitize.ts +0 -143
- package/src/runtime-env.ts +0 -12
- package/src/service.test.ts +0 -337
- package/src/service.ts +0 -2786
- package/src/state.test.ts +0 -119
- package/src/state.ts +0 -206
- package/src/transcript.ts +0 -186
- package/src/types.ts +0 -86
- package/src/utils.ts +0 -74
- package/src/yaml.ts +0 -88
- package/tsconfig.json +0 -15
package/src/memory.test.ts
DELETED
|
@@ -1,588 +0,0 @@
|
|
|
1
|
-
import { MemoryStore, mergeMemoryCandidates } from "./memory.js";
|
|
2
|
-
import type { ParsedMemoryIssue } from "./types.js";
|
|
3
|
-
import { sha256 } from "./utils.js";
|
|
4
|
-
import { stringifyFlatYaml } from "./yaml.js";
|
|
5
|
-
|
|
6
|
-
function memory(overrides: Partial<ParsedMemoryIssue> = {}): ParsedMemoryIssue {
|
|
7
|
-
return {
|
|
8
|
-
issueNumber: overrides.issueNumber ?? 1,
|
|
9
|
-
title: overrides.title ?? "Memory: Example",
|
|
10
|
-
memoryId: overrides.memoryId ?? String(overrides.issueNumber ?? 1),
|
|
11
|
-
date: overrides.date ?? "2026-03-23",
|
|
12
|
-
detail: overrides.detail ?? "Example durable detail",
|
|
13
|
-
status: overrides.status ?? "active",
|
|
14
|
-
...(overrides.kind ? { kind: overrides.kind } : {}),
|
|
15
|
-
...(overrides.memoryHash ? { memoryHash: overrides.memoryHash } : {}),
|
|
16
|
-
...(overrides.topics ? { topics: overrides.topics } : {}),
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
type IssueRecord = { number: number; title?: string; body?: string; state?: "open" | "closed"; labels?: string[] };
|
|
21
|
-
type LabelRecord = { name?: string };
|
|
22
|
-
|
|
23
|
-
function issueFromMemory(m: ParsedMemoryIssue): IssueRecord {
|
|
24
|
-
return {
|
|
25
|
-
number: m.issueNumber,
|
|
26
|
-
title: m.title,
|
|
27
|
-
body: stringifyFlatYaml([
|
|
28
|
-
["memory_hash", m.memoryHash ?? ""],
|
|
29
|
-
["date", m.date],
|
|
30
|
-
["detail", m.detail],
|
|
31
|
-
]),
|
|
32
|
-
state: m.status === "stale" ? "closed" : "open",
|
|
33
|
-
labels: [
|
|
34
|
-
"type:memory",
|
|
35
|
-
...(m.kind ? [`kind:${m.kind}`] : []),
|
|
36
|
-
...(m.topics ?? []).map((topic) => `topic:${topic}`),
|
|
37
|
-
],
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function assert(condition: unknown, message: string): void {
|
|
42
|
-
if (!condition) throw new Error(message);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function testBackendSearchBuildsSingleCleanedQuery(): Promise<void> {
|
|
46
|
-
const queries: string[] = [];
|
|
47
|
-
const client = {
|
|
48
|
-
repo: () => "owner/main-memory",
|
|
49
|
-
searchIssues: async (query: string) => {
|
|
50
|
-
queries.push(query);
|
|
51
|
-
return [] as IssueRecord[];
|
|
52
|
-
},
|
|
53
|
-
};
|
|
54
|
-
const store = new MemoryStore(client as never);
|
|
55
|
-
await store.search([
|
|
56
|
-
"<clawmem-context>",
|
|
57
|
-
"- [11] Previous memory that should be stripped",
|
|
58
|
-
"</clawmem-context>",
|
|
59
|
-
"Conversation info (untrusted metadata):",
|
|
60
|
-
"```json",
|
|
61
|
-
'{"channel":"slack"}',
|
|
62
|
-
"```",
|
|
63
|
-
"",
|
|
64
|
-
"[message_id: abc-123]",
|
|
65
|
-
"",
|
|
66
|
-
"[Slack 2026-04-03 09:30]: Please help debug the Redis rate limiting path.",
|
|
67
|
-
"See https://example.com/debug for more context.",
|
|
68
|
-
"throw new TimeoutError('lua script timeout')",
|
|
69
|
-
"[System: auto-translated]",
|
|
70
|
-
].join("\n"), 5);
|
|
71
|
-
|
|
72
|
-
assert(queries.length === 1, "expected a single backend search query");
|
|
73
|
-
assert(queries[0]?.includes("repo:owner/main-memory"), "expected the backend query to stay scoped to the repo");
|
|
74
|
-
assert(queries[0]?.includes('label:"type:memory"'), "expected the backend query to filter memory issues");
|
|
75
|
-
assert((queries[0] ?? "").length <= 1610, "expected the backend search query to stay within the configured cap plus qualifiers");
|
|
76
|
-
assert(queries[0]?.toLowerCase().includes("redis"), "expected the backend query to retain key terms");
|
|
77
|
-
assert(!queries[0]?.includes("<clawmem-context>"), "expected injected clawmem context to be stripped");
|
|
78
|
-
assert(!queries[0]?.includes("https://example.com/debug"), "expected URLs to be stripped from backend recall");
|
|
79
|
-
assert(!queries[0]?.includes("Conversation info (untrusted metadata):"), "expected inbound metadata blocks to be stripped");
|
|
80
|
-
assert(!queries[0]?.includes("[message_id:"), "expected message id hints to be stripped");
|
|
81
|
-
assert(!queries[0]?.includes("[Slack 2026-04-03 09:30]"), "expected envelope prefixes to be stripped");
|
|
82
|
-
assert(!queries[0]?.includes("[System: auto-translated]"), "expected trailing system hints to be stripped");
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async function testBackendSearchPreferredForRecall(): Promise<void> {
|
|
86
|
-
const searched = [
|
|
87
|
-
issueFromMemory(memory({
|
|
88
|
-
issueNumber: 2,
|
|
89
|
-
title: "Memory: semantic winner",
|
|
90
|
-
detail: "Use Lua scripts to keep Redis rate limiting atomic.",
|
|
91
|
-
kind: "lesson",
|
|
92
|
-
topics: ["redis"],
|
|
93
|
-
})),
|
|
94
|
-
];
|
|
95
|
-
const queries: string[] = [];
|
|
96
|
-
const client = {
|
|
97
|
-
repo: () => "owner/main-memory",
|
|
98
|
-
searchIssues: async (query: string) => {
|
|
99
|
-
queries.push(query);
|
|
100
|
-
return searched;
|
|
101
|
-
},
|
|
102
|
-
};
|
|
103
|
-
const store = new MemoryStore(client as never);
|
|
104
|
-
const found = await store.search("redis rate limiting", 1);
|
|
105
|
-
|
|
106
|
-
assert(queries.length === 1, "expected backend search to be called once");
|
|
107
|
-
assert(queries[0]?.includes('repo:owner/main-memory'), "expected backend query to scope to the current repo");
|
|
108
|
-
assert(queries[0]?.includes('label:\"type:memory\"') || queries[0]?.includes('label:"type:memory"'), "expected backend query to filter memory issues");
|
|
109
|
-
assert(found.length === 1 && found[0]?.issueNumber === 2, "expected backend search results to be preferred");
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async function testBackendSearchReturnsEmptyWithoutLexicalFallback(): Promise<void> {
|
|
113
|
-
const issues = [
|
|
114
|
-
issueFromMemory(memory({
|
|
115
|
-
issueNumber: 3,
|
|
116
|
-
title: "Memory: Redis rate limit tuning",
|
|
117
|
-
detail: "Distributed Redis rate limiting must use Lua scripts to stay atomic.",
|
|
118
|
-
kind: "lesson",
|
|
119
|
-
topics: ["redis"],
|
|
120
|
-
})),
|
|
121
|
-
];
|
|
122
|
-
const client = {
|
|
123
|
-
repo: () => "owner/main-memory",
|
|
124
|
-
listIssues: async () => issues,
|
|
125
|
-
searchIssues: async () => [] as IssueRecord[],
|
|
126
|
-
};
|
|
127
|
-
const store = new MemoryStore(client as never);
|
|
128
|
-
const found = await store.search("redis rate limiting", 5);
|
|
129
|
-
|
|
130
|
-
assert(found.length === 0, "expected backend-only recall to return no results when the backend finds nothing");
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
async function testBackendSearchPropagatesErrors(): Promise<void> {
|
|
134
|
-
const client = {
|
|
135
|
-
repo: () => "owner/main-memory",
|
|
136
|
-
searchIssues: async () => { throw new Error("search unavailable"); },
|
|
137
|
-
};
|
|
138
|
-
const store = new MemoryStore(client as never);
|
|
139
|
-
let message = "";
|
|
140
|
-
try {
|
|
141
|
-
await store.search("redis rate limiting", 5);
|
|
142
|
-
} catch (error) {
|
|
143
|
-
message = String(error);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
assert(message.includes("search unavailable"), "expected backend failures to propagate instead of falling back locally");
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function testMergeMemoryCandidates(): void {
|
|
150
|
-
const merged = mergeMemoryCandidates(
|
|
151
|
-
[
|
|
152
|
-
{
|
|
153
|
-
candidateId: "abc",
|
|
154
|
-
detail: "Redis Lua scripts keep rate limiting atomic.",
|
|
155
|
-
topics: ["redis"],
|
|
156
|
-
},
|
|
157
|
-
],
|
|
158
|
-
[
|
|
159
|
-
{
|
|
160
|
-
candidateId: "abc",
|
|
161
|
-
detail: "Redis Lua scripts keep rate limiting atomic.",
|
|
162
|
-
kind: "lesson",
|
|
163
|
-
topics: ["rate-limit"],
|
|
164
|
-
evidence: "User confirmed the production path uses Lua.",
|
|
165
|
-
},
|
|
166
|
-
],
|
|
167
|
-
);
|
|
168
|
-
|
|
169
|
-
assert(merged.length === 1, "expected duplicate candidates to merge by candidateId");
|
|
170
|
-
assert(merged[0]?.kind === "lesson", "expected merged candidates to preserve new schema hints");
|
|
171
|
-
assert(JSON.stringify(merged[0]?.topics) === JSON.stringify(["rate-limit", "redis"]), "expected merged candidates to union topics");
|
|
172
|
-
assert(merged[0]?.evidence === "User confirmed the production path uses Lua.", "expected merged candidates to preserve evidence");
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
async function testStructuredStoreAndSchema(): Promise<void> {
|
|
176
|
-
const created: Array<{ title: string; body: string; labels: string[] }> = [];
|
|
177
|
-
const ensured: string[][] = [];
|
|
178
|
-
const labels: LabelRecord[] = [{ name: "kind:lesson" }, { name: "topic:redis" }];
|
|
179
|
-
const client = {
|
|
180
|
-
repo: () => "owner/main-memory",
|
|
181
|
-
searchIssues: async () => [] as IssueRecord[],
|
|
182
|
-
listIssues: async () => [] as IssueRecord[],
|
|
183
|
-
listLabels: async () => labels,
|
|
184
|
-
ensureLabels: async (next: string[]) => { ensured.push(next); },
|
|
185
|
-
createIssue: async (payload: { title: string; body: string; labels: string[] }) => {
|
|
186
|
-
created.push(payload);
|
|
187
|
-
return { number: 99, title: payload.title };
|
|
188
|
-
},
|
|
189
|
-
};
|
|
190
|
-
const store = new MemoryStore(client as never);
|
|
191
|
-
const result = await store.store({ detail: "Redis Lua scripts are required for atomic rate limiting.", kind: "Lesson", topics: ["Redis Ops", "rate_limit"] });
|
|
192
|
-
const schema = await store.listSchema();
|
|
193
|
-
|
|
194
|
-
assert(result.created === true, "expected a new structured memory to be created");
|
|
195
|
-
assert(result.memory.kind === "lesson", "expected kind to be normalized");
|
|
196
|
-
assert(JSON.stringify(result.memory.topics) === JSON.stringify(["rate-limit", "redis-ops"]), "expected topics to be normalized and sorted");
|
|
197
|
-
assert(created.length === 1, "expected a single issue creation");
|
|
198
|
-
assert(created[0]?.labels.includes("kind:lesson"), "expected created labels to include normalized kind");
|
|
199
|
-
assert(created[0]?.labels.includes("topic:redis-ops"), "expected created labels to include normalized topic");
|
|
200
|
-
assert(created[0]?.labels.includes("topic:rate-limit"), "expected created labels to include normalized topic");
|
|
201
|
-
assert(!created[0]?.labels.some((label) => label.startsWith("session:")), "expected manual memory_store writes to omit synthetic session labels");
|
|
202
|
-
assert(!created[0]?.labels.some((label) => label.startsWith("date:")), "expected new memory labels to omit date labels");
|
|
203
|
-
assert(created[0]?.body.includes("memory_hash:"), "expected new memory body to retain metadata fields");
|
|
204
|
-
assert(created[0]?.body.includes("detail: Redis Lua scripts are required for atomic rate limiting."), "expected new memory body to store detail in YAML");
|
|
205
|
-
assert(created[0]?.body.includes(`date: ${result.memory.date}`), "expected new memory body to retain logical date metadata");
|
|
206
|
-
assert(ensured[0]?.includes("kind:lesson"), "expected ensureLabels to include kind label");
|
|
207
|
-
assert(schema.kinds.includes("lesson"), "expected schema to expose existing kind labels");
|
|
208
|
-
assert(schema.topics.includes("redis"), "expected schema to expose existing topic labels");
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
async function testListSchemaPrefersLabelsWithoutIssueScan(): Promise<void> {
|
|
212
|
-
const client = {
|
|
213
|
-
listLabels: async () => [{ name: "kind:lesson" }, { name: "topic:redis" }, { name: "topic:rate-limit" }],
|
|
214
|
-
listIssues: async () => { throw new Error("listSchema should not scan issues when label schema is available"); },
|
|
215
|
-
};
|
|
216
|
-
const store = new MemoryStore(client as never);
|
|
217
|
-
const schema = await store.listSchema();
|
|
218
|
-
|
|
219
|
-
assert(JSON.stringify(schema.kinds) === JSON.stringify(["lesson"]), "expected schema kinds to come from labels");
|
|
220
|
-
assert(JSON.stringify(schema.topics) === JSON.stringify(["rate-limit", "redis"]), "expected schema topics to come from labels");
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
async function testStoreDeduplicatesViaHashSearch(): Promise<void> {
|
|
224
|
-
const detail = "Redis Lua scripts are required for atomic rate limiting.";
|
|
225
|
-
const hash = sha256(detail);
|
|
226
|
-
const existing = memory({
|
|
227
|
-
issueNumber: 77,
|
|
228
|
-
detail,
|
|
229
|
-
memoryHash: hash,
|
|
230
|
-
kind: "lesson",
|
|
231
|
-
topics: ["redis-ops"],
|
|
232
|
-
});
|
|
233
|
-
const queries: string[] = [];
|
|
234
|
-
const client = {
|
|
235
|
-
repo: () => "owner/main-memory",
|
|
236
|
-
searchIssues: async (query: string) => {
|
|
237
|
-
queries.push(query);
|
|
238
|
-
return [issueFromMemory(existing)];
|
|
239
|
-
},
|
|
240
|
-
listIssues: async () => { throw new Error("store should not scan all active memories"); },
|
|
241
|
-
ensureLabels: async () => {},
|
|
242
|
-
createIssue: async () => { throw new Error("store should not create a duplicate issue"); },
|
|
243
|
-
};
|
|
244
|
-
const store = new MemoryStore(client as never);
|
|
245
|
-
const result = await store.store({ detail, kind: "lesson", topics: ["redis_ops"] });
|
|
246
|
-
|
|
247
|
-
assert(result.created === false, "expected hash search to reuse an existing exact duplicate");
|
|
248
|
-
assert(result.memory.issueNumber === 77, "expected hash search to return the existing memory");
|
|
249
|
-
assert(queries.length === 1 && queries[0]?.includes(hash), "expected store to query by memory hash");
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
async function testStoreKeepsFullAutoTitleAndSupportsExplicitTitle(): Promise<void> {
|
|
253
|
-
const created: Array<{ title: string; body: string; labels: string[] }> = [];
|
|
254
|
-
const client = {
|
|
255
|
-
listIssues: async () => [] as IssueRecord[],
|
|
256
|
-
listLabels: async () => [] as LabelRecord[],
|
|
257
|
-
ensureLabels: async () => {},
|
|
258
|
-
createIssue: async (payload: { title: string; body: string; labels: string[] }) => {
|
|
259
|
-
created.push(payload);
|
|
260
|
-
return { number: created.length + 100, title: payload.title };
|
|
261
|
-
},
|
|
262
|
-
};
|
|
263
|
-
const store = new MemoryStore(client as never);
|
|
264
|
-
const longDetail = "Tech Decision #001: Frontend = React Native, Backend = FastAPI, Database = PostgreSQL, and analytics events must stay append-only for auditability.";
|
|
265
|
-
const auto = await store.store({ detail: longDetail });
|
|
266
|
-
const explicit = await store.store({ title: "Architecture Decision #001", detail: "Use React Native + FastAPI for the first mobile stack." });
|
|
267
|
-
|
|
268
|
-
assert(auto.memory.title === `Memory: ${longDetail}`, "expected auto-generated memory title to keep the full detail without truncation");
|
|
269
|
-
assert(explicit.memory.title === "Memory: Architecture Decision #001", "expected explicit memory title to be preserved");
|
|
270
|
-
assert(created[0]?.title === `Memory: ${longDetail}`, "expected created issue title to keep the full auto title");
|
|
271
|
-
assert(created[1]?.title === "Memory: Architecture Decision #001", "expected created issue title to use the explicit title");
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
async function testGetAndListMemories(): Promise<void> {
|
|
275
|
-
const issues = [
|
|
276
|
-
issueFromMemory(memory({
|
|
277
|
-
issueNumber: 4,
|
|
278
|
-
title: "Memory: xiangz preferences",
|
|
279
|
-
detail: "xiangz likes F1 and watches Dota 2 as a viewer.",
|
|
280
|
-
kind: "core-fact",
|
|
281
|
-
topics: ["preferences", "hobbies"],
|
|
282
|
-
})),
|
|
283
|
-
issueFromMemory(memory({
|
|
284
|
-
issueNumber: 10,
|
|
285
|
-
title: "Memory: fruit preference",
|
|
286
|
-
detail: "xiangz likes mango.",
|
|
287
|
-
kind: "core-fact",
|
|
288
|
-
topics: ["food"],
|
|
289
|
-
})),
|
|
290
|
-
issueFromMemory(memory({
|
|
291
|
-
issueNumber: 11,
|
|
292
|
-
title: "Memory: old sports note",
|
|
293
|
-
detail: "xiangz follows F1.",
|
|
294
|
-
kind: "lesson",
|
|
295
|
-
status: "stale",
|
|
296
|
-
topics: ["sports"],
|
|
297
|
-
})),
|
|
298
|
-
];
|
|
299
|
-
const client = {
|
|
300
|
-
getIssue: async (number: number) => {
|
|
301
|
-
const issue = issues.find((entry) => entry.number === number);
|
|
302
|
-
if (!issue) throw new Error("issue missing");
|
|
303
|
-
return issue;
|
|
304
|
-
},
|
|
305
|
-
listIssues: async (params?: { labels?: string[]; state?: "open" | "closed" | "all" }) => {
|
|
306
|
-
const labels = params?.labels ?? [];
|
|
307
|
-
const state = params?.state ?? "open";
|
|
308
|
-
return issues.filter((issue) => {
|
|
309
|
-
const issueLabels = issue.labels ?? [];
|
|
310
|
-
if (!labels.every((label) => issueLabels.includes(label))) return false;
|
|
311
|
-
if (state === "all") return true;
|
|
312
|
-
return (issue.state ?? "open") === state;
|
|
313
|
-
});
|
|
314
|
-
},
|
|
315
|
-
};
|
|
316
|
-
const store = new MemoryStore(client as never);
|
|
317
|
-
const exact = await store.get("4");
|
|
318
|
-
const activeFacts = await store.listMemories({ status: "active", kind: "core-fact", limit: 10 });
|
|
319
|
-
const sports = await store.listMemories({ status: "all", topic: "sports", limit: 10 });
|
|
320
|
-
|
|
321
|
-
assert(exact?.issueNumber === 4, "expected direct memory lookup to find issue #4");
|
|
322
|
-
assert(activeFacts.length === 2, "expected listMemories to filter active core facts");
|
|
323
|
-
assert(activeFacts[0]?.issueNumber === 10, "expected listMemories to sort newest-first");
|
|
324
|
-
assert(sports.length === 1 && sports[0]?.issueNumber === 11, "expected listMemories to filter by topic across statuses");
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
async function testLegacyMemoriesWithoutSessionOrDate(): Promise<void> {
|
|
328
|
-
const issues: IssueRecord[] = [
|
|
329
|
-
{
|
|
330
|
-
number: 4,
|
|
331
|
-
title: "Memory: xiangz preferences",
|
|
332
|
-
body: "xiangz likes F1 and watches Dota 2 as a viewer.",
|
|
333
|
-
labels: ["type:memory", "kind:core-fact", "topic:preferences"],
|
|
334
|
-
},
|
|
335
|
-
];
|
|
336
|
-
const client = {
|
|
337
|
-
repo: () => "owner/main-memory",
|
|
338
|
-
getIssue: async (number: number) => {
|
|
339
|
-
const issue = issues.find((entry) => entry.number === number);
|
|
340
|
-
if (!issue) throw new Error("issue missing");
|
|
341
|
-
return issue;
|
|
342
|
-
},
|
|
343
|
-
listIssues: async (params?: { labels?: string[]; state?: "open" | "closed" | "all" }) => {
|
|
344
|
-
const labels = params?.labels ?? [];
|
|
345
|
-
const state = params?.state ?? "open";
|
|
346
|
-
return issues.filter((issue) => {
|
|
347
|
-
const issueLabels = issue.labels ?? [];
|
|
348
|
-
if (!labels.every((label) => issueLabels.includes(label))) return false;
|
|
349
|
-
if (state === "all") return true;
|
|
350
|
-
return (issue.state ?? "open") === state;
|
|
351
|
-
});
|
|
352
|
-
},
|
|
353
|
-
searchIssues: async () => issues,
|
|
354
|
-
};
|
|
355
|
-
const store = new MemoryStore(client as never);
|
|
356
|
-
const exact = await store.get("4");
|
|
357
|
-
const recalled = await store.search("F1 Dota 2", 5);
|
|
358
|
-
|
|
359
|
-
assert(exact?.issueNumber === 4, "expected legacy memory without session/date to be readable");
|
|
360
|
-
assert(exact?.date === "1970-01-01", "expected missing date label to fall back to a placeholder");
|
|
361
|
-
assert(recalled.some((memory) => memory.issueNumber === 4), "expected legacy memory to participate in recall");
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
async function testUpdateMemoryInPlace(): Promise<void> {
|
|
365
|
-
const issues: IssueRecord[] = [
|
|
366
|
-
issueFromMemory(memory({
|
|
367
|
-
issueNumber: 4,
|
|
368
|
-
title: "Memory: xiangz preferences",
|
|
369
|
-
detail: "xiangz likes F1 and watches Dota 2 as a viewer.",
|
|
370
|
-
kind: "core-fact",
|
|
371
|
-
topics: ["preferences"],
|
|
372
|
-
})),
|
|
373
|
-
];
|
|
374
|
-
const ensured: string[][] = [];
|
|
375
|
-
const updatedIssues: Array<{ number: number; title?: string; body?: string }> = [];
|
|
376
|
-
const syncedLabels: Array<{ number: number; labels: string[] }> = [];
|
|
377
|
-
const client = {
|
|
378
|
-
getIssue: async (number: number) => {
|
|
379
|
-
const issue = issues.find((entry) => entry.number === number);
|
|
380
|
-
if (!issue) throw new Error("issue missing");
|
|
381
|
-
return issue;
|
|
382
|
-
},
|
|
383
|
-
listIssues: async (params?: { labels?: string[]; state?: "open" | "closed" | "all" }) => {
|
|
384
|
-
const labels = params?.labels ?? [];
|
|
385
|
-
const state = params?.state ?? "open";
|
|
386
|
-
return issues.filter((issue) => {
|
|
387
|
-
const issueLabels = issue.labels ?? [];
|
|
388
|
-
if (!labels.every((label) => issueLabels.includes(label))) return false;
|
|
389
|
-
if (state === "all") return true;
|
|
390
|
-
return (issue.state ?? "open") === state;
|
|
391
|
-
});
|
|
392
|
-
},
|
|
393
|
-
ensureLabels: async (labels: string[]) => { ensured.push(labels); },
|
|
394
|
-
updateIssue: async (number: number, patch: { title?: string; body?: string }) => {
|
|
395
|
-
updatedIssues.push({ number, ...patch });
|
|
396
|
-
const issue = issues.find((entry) => entry.number === number);
|
|
397
|
-
if (!issue) throw new Error("issue missing");
|
|
398
|
-
if (patch.title) issue.title = patch.title;
|
|
399
|
-
if (patch.body) issue.body = patch.body;
|
|
400
|
-
return issue;
|
|
401
|
-
},
|
|
402
|
-
syncManagedLabels: async (number: number, labels: string[]) => {
|
|
403
|
-
syncedLabels.push({ number, labels });
|
|
404
|
-
const issue = issues.find((entry) => entry.number === number);
|
|
405
|
-
if (!issue) throw new Error("issue missing");
|
|
406
|
-
issue.labels = labels;
|
|
407
|
-
},
|
|
408
|
-
};
|
|
409
|
-
const store = new MemoryStore(client as never);
|
|
410
|
-
const updated = await store.update("4", {
|
|
411
|
-
detail: "xiangz likes F1, watches Dota 2 as a viewer, and recently follows tennis.",
|
|
412
|
-
topics: ["preferences", "sports"],
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
assert(updated?.issueNumber === 4, "expected memory_update to modify the same issue");
|
|
416
|
-
assert(updated?.detail.includes("tennis"), "expected updated detail to be returned");
|
|
417
|
-
assert(JSON.stringify(updated?.topics) === JSON.stringify(["preferences", "sports"]), "expected topics to be replaced");
|
|
418
|
-
assert(updatedIssues.length === 1, "expected a single issue update");
|
|
419
|
-
assert(updatedIssues[0]?.title !== "Memory: xiangz preferences", "expected title to refresh from updated detail");
|
|
420
|
-
assert(updatedIssues[0]?.body?.includes("memory_hash:"), "expected updated body to retain metadata");
|
|
421
|
-
assert(updatedIssues[0]?.body?.includes("detail:"), "expected updated body to store a detail field in YAML");
|
|
422
|
-
assert(updatedIssues[0]?.body?.includes("recently follows tennis"), "expected updated body to contain the updated detail text");
|
|
423
|
-
assert(ensured[0]?.includes("topic:sports"), "expected new topic label to be ensured");
|
|
424
|
-
assert(syncedLabels[0]?.labels.includes("kind:core-fact"), "expected existing kind label to be preserved");
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
async function testUpdateSupportsExplicitRetitle(): Promise<void> {
|
|
428
|
-
const issues: IssueRecord[] = [
|
|
429
|
-
issueFromMemory(memory({
|
|
430
|
-
issueNumber: 20,
|
|
431
|
-
title: "Memory: old short title",
|
|
432
|
-
detail: "We use append-only audit events for billing changes.",
|
|
433
|
-
kind: "convention",
|
|
434
|
-
})),
|
|
435
|
-
];
|
|
436
|
-
const updatedIssues: Array<{ number: number; title?: string; body?: string }> = [];
|
|
437
|
-
const client = {
|
|
438
|
-
getIssue: async (number: number) => {
|
|
439
|
-
const issue = issues.find((entry) => entry.number === number);
|
|
440
|
-
if (!issue) throw new Error("issue missing");
|
|
441
|
-
return issue;
|
|
442
|
-
},
|
|
443
|
-
listIssues: async (params?: { labels?: string[]; state?: "open" | "closed" | "all" }) => {
|
|
444
|
-
const labels = params?.labels ?? [];
|
|
445
|
-
const state = params?.state ?? "open";
|
|
446
|
-
return issues.filter((issue) => {
|
|
447
|
-
const issueLabels = issue.labels ?? [];
|
|
448
|
-
if (!labels.every((label) => issueLabels.includes(label))) return false;
|
|
449
|
-
if (state === "all") return true;
|
|
450
|
-
return (issue.state ?? "open") === state;
|
|
451
|
-
});
|
|
452
|
-
},
|
|
453
|
-
ensureLabels: async () => {},
|
|
454
|
-
updateIssue: async (number: number, patch: { title?: string; body?: string }) => {
|
|
455
|
-
updatedIssues.push({ number, ...patch });
|
|
456
|
-
const issue = issues.find((entry) => entry.number === number);
|
|
457
|
-
if (!issue) throw new Error("issue missing");
|
|
458
|
-
if (patch.title) issue.title = patch.title;
|
|
459
|
-
if (patch.body) issue.body = patch.body;
|
|
460
|
-
return issue;
|
|
461
|
-
},
|
|
462
|
-
syncManagedLabels: async () => {},
|
|
463
|
-
};
|
|
464
|
-
const store = new MemoryStore(client as never);
|
|
465
|
-
const updated = await store.update("20", { title: "Billing Audit Convention" });
|
|
466
|
-
|
|
467
|
-
assert(updated?.title === "Memory: Billing Audit Convention", "expected memory_update to support explicit retitle");
|
|
468
|
-
assert(updatedIssues[0]?.title === "Memory: Billing Audit Convention", "expected issue title patch to use the explicit retitle");
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
async function testUpdateUsesHashSearchForDuplicateCheck(): Promise<void> {
|
|
472
|
-
const currentDetail = "We use append-only audit events for billing changes.";
|
|
473
|
-
const conflictingDetail = "Billing events must stay append-only for auditability.";
|
|
474
|
-
const current = issueFromMemory(memory({
|
|
475
|
-
issueNumber: 20,
|
|
476
|
-
title: "Memory: billing convention",
|
|
477
|
-
detail: currentDetail,
|
|
478
|
-
memoryHash: sha256(currentDetail),
|
|
479
|
-
kind: "convention",
|
|
480
|
-
}));
|
|
481
|
-
const conflicting = issueFromMemory(memory({
|
|
482
|
-
issueNumber: 21,
|
|
483
|
-
title: "Memory: audit rule",
|
|
484
|
-
detail: conflictingDetail,
|
|
485
|
-
memoryHash: sha256(conflictingDetail),
|
|
486
|
-
kind: "convention",
|
|
487
|
-
}));
|
|
488
|
-
const queries: string[] = [];
|
|
489
|
-
const client = {
|
|
490
|
-
repo: () => "owner/main-memory",
|
|
491
|
-
getIssue: async (number: number) => {
|
|
492
|
-
if (number === 20) return current;
|
|
493
|
-
throw new Error("issue missing");
|
|
494
|
-
},
|
|
495
|
-
searchIssues: async (query: string) => {
|
|
496
|
-
queries.push(query);
|
|
497
|
-
return [conflicting];
|
|
498
|
-
},
|
|
499
|
-
listIssues: async () => { throw new Error("update should not scan all active memories when direct lookup/search are available"); },
|
|
500
|
-
ensureLabels: async () => {},
|
|
501
|
-
updateIssue: async () => { throw new Error("duplicate update should fail before mutating"); },
|
|
502
|
-
syncManagedLabels: async () => {},
|
|
503
|
-
};
|
|
504
|
-
const store = new MemoryStore(client as never);
|
|
505
|
-
let message = "";
|
|
506
|
-
try {
|
|
507
|
-
await store.update("20", { detail: conflictingDetail });
|
|
508
|
-
} catch (error) {
|
|
509
|
-
message = String(error);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
assert(message.includes("[21]"), "expected duplicate detection to reference the conflicting memory");
|
|
513
|
-
assert(queries.length === 1 && queries[0]?.includes(sha256(conflictingDetail)), "expected update duplicate checks to search by memory hash");
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
async function testForgetClosesMemoryIssue(): Promise<void> {
|
|
517
|
-
const issues: IssueRecord[] = [
|
|
518
|
-
issueFromMemory(memory({
|
|
519
|
-
issueNumber: 12,
|
|
520
|
-
title: "Memory: outdated deployment rule",
|
|
521
|
-
detail: "Always restart the full cluster after deploy.",
|
|
522
|
-
kind: "convention",
|
|
523
|
-
topics: ["deploy"],
|
|
524
|
-
})),
|
|
525
|
-
];
|
|
526
|
-
const syncedLabels: Array<{ number: number; labels: string[] }> = [];
|
|
527
|
-
const updatedIssues: Array<{ number: number; state?: "open" | "closed" }> = [];
|
|
528
|
-
const client = {
|
|
529
|
-
getIssue: async (number: number) => {
|
|
530
|
-
const issue = issues.find((entry) => entry.number === number);
|
|
531
|
-
if (!issue) throw new Error("issue missing");
|
|
532
|
-
return issue;
|
|
533
|
-
},
|
|
534
|
-
listIssues: async (params?: { labels?: string[]; state?: "open" | "closed" | "all" }) => {
|
|
535
|
-
const labels = params?.labels ?? [];
|
|
536
|
-
const state = params?.state ?? "open";
|
|
537
|
-
return issues.filter((issue) => {
|
|
538
|
-
const issueLabels = issue.labels ?? [];
|
|
539
|
-
if (!labels.every((label) => issueLabels.includes(label))) return false;
|
|
540
|
-
if (state === "all") return true;
|
|
541
|
-
return (issue.state ?? "open") === state;
|
|
542
|
-
});
|
|
543
|
-
},
|
|
544
|
-
syncManagedLabels: async (number: number, labels: string[]) => {
|
|
545
|
-
syncedLabels.push({ number, labels });
|
|
546
|
-
const issue = issues.find((entry) => entry.number === number);
|
|
547
|
-
if (!issue) throw new Error("issue missing");
|
|
548
|
-
issue.labels = labels;
|
|
549
|
-
},
|
|
550
|
-
updateIssue: async (number: number, patch: { state?: "open" | "closed" }) => {
|
|
551
|
-
updatedIssues.push({ number, state: patch.state });
|
|
552
|
-
const issue = issues.find((entry) => entry.number === number);
|
|
553
|
-
if (!issue) throw new Error("issue missing");
|
|
554
|
-
if (patch.state) issue.state = patch.state;
|
|
555
|
-
return issue;
|
|
556
|
-
},
|
|
557
|
-
};
|
|
558
|
-
const store = new MemoryStore(client as never);
|
|
559
|
-
const forgotten = await store.forget("12");
|
|
560
|
-
|
|
561
|
-
assert(forgotten?.status === "stale", "expected forgotten memory to be returned as stale");
|
|
562
|
-
assert(updatedIssues[0]?.state === "closed", "expected memory_forget to close the issue");
|
|
563
|
-
assert(syncedLabels[0]?.labels.every((label) => !label.startsWith("memory-status:")), "expected memory_forget to stop writing lifecycle labels");
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
async function main(): Promise<void> {
|
|
567
|
-
await testBackendSearchBuildsSingleCleanedQuery();
|
|
568
|
-
await testBackendSearchPreferredForRecall();
|
|
569
|
-
await testBackendSearchReturnsEmptyWithoutLexicalFallback();
|
|
570
|
-
await testBackendSearchPropagatesErrors();
|
|
571
|
-
testMergeMemoryCandidates();
|
|
572
|
-
await testStructuredStoreAndSchema();
|
|
573
|
-
await testListSchemaPrefersLabelsWithoutIssueScan();
|
|
574
|
-
await testStoreDeduplicatesViaHashSearch();
|
|
575
|
-
await testStoreKeepsFullAutoTitleAndSupportsExplicitTitle();
|
|
576
|
-
await testGetAndListMemories();
|
|
577
|
-
await testLegacyMemoriesWithoutSessionOrDate();
|
|
578
|
-
await testUpdateMemoryInPlace();
|
|
579
|
-
await testUpdateSupportsExplicitRetitle();
|
|
580
|
-
await testForgetClosesMemoryIssue();
|
|
581
|
-
await testUpdateUsesHashSearchForDuplicateCheck();
|
|
582
|
-
console.log("memory tests passed");
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
main().catch((error) => {
|
|
586
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
587
|
-
process.exit(1);
|
|
588
|
-
});
|