@clawmem-ai/clawmem 0.1.16 → 0.1.17
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 +5 -7
- package/openclaw.plugin.json +4 -31
- package/package.json +12 -5
- package/skills/clawmem/SKILL.md +5 -5
- package/skills/clawmem/references/collaboration.md +43 -1
- package/skills/clawmem/references/schema.md +2 -1
- package/src/config.test.ts +0 -3
- package/src/config.ts +0 -3
- package/src/conversation.test.ts +63 -13
- package/src/conversation.ts +89 -392
- package/src/github-client.test.ts +101 -0
- package/src/github-client.ts +59 -0
- package/src/memory.test.ts +131 -46
- package/src/memory.ts +81 -392
- package/src/service.test.ts +118 -0
- package/src/service.ts +728 -419
- package/src/state.test.ts +47 -16
- package/src/state.ts +87 -119
- package/src/types.ts +9 -26
package/src/github-client.ts
CHANGED
|
@@ -52,6 +52,12 @@ type RepositoryInvitationResponse = {
|
|
|
52
52
|
inviter?: { login?: string; name?: string };
|
|
53
53
|
};
|
|
54
54
|
type TeamMembershipResponse = { state?: string; role?: string };
|
|
55
|
+
type OrganizationMembershipResponse = {
|
|
56
|
+
state?: string;
|
|
57
|
+
role?: string;
|
|
58
|
+
organization?: OrgResponse;
|
|
59
|
+
user?: CollaboratorResponse;
|
|
60
|
+
};
|
|
55
61
|
type InvitationResponse = {
|
|
56
62
|
id?: number;
|
|
57
63
|
role?: string;
|
|
@@ -145,9 +151,30 @@ export class GitHubIssueClient {
|
|
|
145
151
|
async getOrg(org: string): Promise<OrgResponse> {
|
|
146
152
|
return this.req<OrgResponse>(`orgs/${encodeURIComponent(org)}`, { method: "GET" });
|
|
147
153
|
}
|
|
154
|
+
async listOrgMembers(org: string, role?: "admin"): Promise<CollaboratorResponse[]> {
|
|
155
|
+
const q = new URLSearchParams();
|
|
156
|
+
if (role) q.set("role", role);
|
|
157
|
+
const suffix = q.toString();
|
|
158
|
+
return this.req<CollaboratorResponse[]>(`orgs/${encodeURIComponent(org)}/members${suffix ? `?${suffix}` : ""}`, { method: "GET" });
|
|
159
|
+
}
|
|
160
|
+
async getOrgMembership(org: string, username: string): Promise<OrganizationMembershipResponse> {
|
|
161
|
+
return this.req<OrganizationMembershipResponse>(
|
|
162
|
+
`orgs/${encodeURIComponent(org)}/memberships/${encodeURIComponent(username)}`,
|
|
163
|
+
{ method: "GET" },
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
async removeOrgMember(org: string, username: string): Promise<void> {
|
|
167
|
+
await this.req(`orgs/${encodeURIComponent(org)}/members/${encodeURIComponent(username)}`, { method: "DELETE" });
|
|
168
|
+
}
|
|
169
|
+
async removeOrgMembership(org: string, username: string): Promise<void> {
|
|
170
|
+
await this.req(`orgs/${encodeURIComponent(org)}/memberships/${encodeURIComponent(username)}`, { method: "DELETE" });
|
|
171
|
+
}
|
|
148
172
|
async listOrgTeams(org: string): Promise<TeamResponse[]> {
|
|
149
173
|
return this.req<TeamResponse[]>(`orgs/${encodeURIComponent(org)}/teams`, { method: "GET" });
|
|
150
174
|
}
|
|
175
|
+
async getTeam(org: string, teamSlug: string): Promise<TeamResponse> {
|
|
176
|
+
return this.req<TeamResponse>(`orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(teamSlug)}`, { method: "GET" });
|
|
177
|
+
}
|
|
151
178
|
async createOrgTeam(org: string, params: { name: string; description?: string; privacy?: "closed" | "secret" }): Promise<TeamResponse> {
|
|
152
179
|
return this.req<TeamResponse>(`orgs/${encodeURIComponent(org)}/teams`, {
|
|
153
180
|
method: "POST",
|
|
@@ -158,6 +185,29 @@ export class GitHubIssueClient {
|
|
|
158
185
|
}),
|
|
159
186
|
});
|
|
160
187
|
}
|
|
188
|
+
async updateTeam(
|
|
189
|
+
org: string,
|
|
190
|
+
teamSlug: string,
|
|
191
|
+
params: { name?: string; description?: string; privacy?: "closed" | "secret" },
|
|
192
|
+
): Promise<TeamResponse> {
|
|
193
|
+
return this.req<TeamResponse>(`orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(teamSlug)}`, {
|
|
194
|
+
method: "PATCH",
|
|
195
|
+
body: JSON.stringify({
|
|
196
|
+
...(params.name ? { name: params.name } : {}),
|
|
197
|
+
...(params.description ? { description: params.description } : {}),
|
|
198
|
+
...(params.privacy ? { privacy: params.privacy } : {}),
|
|
199
|
+
}),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
async deleteTeam(org: string, teamSlug: string): Promise<void> {
|
|
203
|
+
await this.req(`orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(teamSlug)}`, { method: "DELETE" });
|
|
204
|
+
}
|
|
205
|
+
async listTeamMembers(org: string, teamSlug: string): Promise<CollaboratorResponse[]> {
|
|
206
|
+
return this.req<CollaboratorResponse[]>(
|
|
207
|
+
`orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(teamSlug)}/members`,
|
|
208
|
+
{ method: "GET" },
|
|
209
|
+
);
|
|
210
|
+
}
|
|
161
211
|
async setTeamMembership(org: string, teamSlug: string, username: string, role: "member" | "maintainer"): Promise<TeamMembershipResponse> {
|
|
162
212
|
return this.req<TeamMembershipResponse>(
|
|
163
213
|
`orgs/${encodeURIComponent(org)}/teams/${encodeURIComponent(teamSlug)}/memberships/${encodeURIComponent(username)}`,
|
|
@@ -238,6 +288,9 @@ export class GitHubIssueClient {
|
|
|
238
288
|
}),
|
|
239
289
|
});
|
|
240
290
|
}
|
|
291
|
+
async revokeOrgInvitation(org: string, invitationId: number): Promise<void> {
|
|
292
|
+
await this.req(`orgs/${encodeURIComponent(org)}/invitations/${invitationId}`, { method: "DELETE" });
|
|
293
|
+
}
|
|
241
294
|
async listOrgOutsideCollaborators(org: string): Promise<CollaboratorResponse[]> {
|
|
242
295
|
return this.req<CollaboratorResponse[]>(`orgs/${encodeURIComponent(org)}/outside_collaborators`, { method: "GET" });
|
|
243
296
|
}
|
|
@@ -250,6 +303,12 @@ export class GitHubIssueClient {
|
|
|
250
303
|
async declineUserOrgInvitation(invitationId: number): Promise<void> {
|
|
251
304
|
await this.req(`user/organization_invitations/${invitationId}`, { method: "DELETE" });
|
|
252
305
|
}
|
|
306
|
+
async transferRepo(owner: string, repo: string, newOwner: string): Promise<RepoResponse> {
|
|
307
|
+
return this.req<RepoResponse>(`repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/transfer`, {
|
|
308
|
+
method: "POST",
|
|
309
|
+
body: JSON.stringify({ new_owner: newOwner }),
|
|
310
|
+
});
|
|
311
|
+
}
|
|
253
312
|
async ensureLabels(labels: string[]): Promise<void> {
|
|
254
313
|
for (const label of labels) {
|
|
255
314
|
if (!label.trim()) continue;
|
package/src/memory.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { MemoryStore, mergeMemoryCandidates
|
|
1
|
+
import { MemoryStore, mergeMemoryCandidates } from "./memory.js";
|
|
2
2
|
import type { ParsedMemoryIssue } from "./types.js";
|
|
3
|
+
import { sha256 } from "./utils.js";
|
|
3
4
|
import { stringifyFlatYaml } from "./yaml.js";
|
|
4
5
|
|
|
5
6
|
function memory(overrides: Partial<ParsedMemoryIssue> = {}): ParsedMemoryIssue {
|
|
@@ -41,18 +42,6 @@ function assert(condition: unknown, message: string): void {
|
|
|
41
42
|
if (!condition) throw new Error(message);
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
function testConfig(): never {
|
|
45
|
-
return {
|
|
46
|
-
memoryRecallLimit: 5,
|
|
47
|
-
memoryAutoRecallLimit: 3,
|
|
48
|
-
turnCommentDelayMs: 1000,
|
|
49
|
-
digestWaitTimeoutMs: 30000,
|
|
50
|
-
summaryWaitTimeoutMs: 120000,
|
|
51
|
-
memoryExtractWaitTimeoutMs: 45000,
|
|
52
|
-
memoryReconcileWaitTimeoutMs: 45000,
|
|
53
|
-
} as never;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
45
|
async function testBackendSearchBuildsSingleCleanedQuery(): Promise<void> {
|
|
57
46
|
const queries: string[] = [];
|
|
58
47
|
const client = {
|
|
@@ -62,7 +51,7 @@ async function testBackendSearchBuildsSingleCleanedQuery(): Promise<void> {
|
|
|
62
51
|
return [] as IssueRecord[];
|
|
63
52
|
},
|
|
64
53
|
};
|
|
65
|
-
const store = new MemoryStore(client as never
|
|
54
|
+
const store = new MemoryStore(client as never);
|
|
66
55
|
await store.search([
|
|
67
56
|
"<clawmem-context>",
|
|
68
57
|
"- [11] Previous memory that should be stripped",
|
|
@@ -111,7 +100,7 @@ async function testBackendSearchPreferredForRecall(): Promise<void> {
|
|
|
111
100
|
return searched;
|
|
112
101
|
},
|
|
113
102
|
};
|
|
114
|
-
const store = new MemoryStore(client as never
|
|
103
|
+
const store = new MemoryStore(client as never);
|
|
115
104
|
const found = await store.search("redis rate limiting", 1);
|
|
116
105
|
|
|
117
106
|
assert(queries.length === 1, "expected backend search to be called once");
|
|
@@ -135,7 +124,7 @@ async function testBackendSearchReturnsEmptyWithoutLexicalFallback(): Promise<vo
|
|
|
135
124
|
listIssues: async () => issues,
|
|
136
125
|
searchIssues: async () => [] as IssueRecord[],
|
|
137
126
|
};
|
|
138
|
-
const store = new MemoryStore(client as never
|
|
127
|
+
const store = new MemoryStore(client as never);
|
|
139
128
|
const found = await store.search("redis rate limiting", 5);
|
|
140
129
|
|
|
141
130
|
assert(found.length === 0, "expected backend-only recall to return no results when the backend finds nothing");
|
|
@@ -146,7 +135,7 @@ async function testBackendSearchPropagatesErrors(): Promise<void> {
|
|
|
146
135
|
repo: () => "owner/main-memory",
|
|
147
136
|
searchIssues: async () => { throw new Error("search unavailable"); },
|
|
148
137
|
};
|
|
149
|
-
const store = new MemoryStore(client as never
|
|
138
|
+
const store = new MemoryStore(client as never);
|
|
150
139
|
let message = "";
|
|
151
140
|
try {
|
|
152
141
|
await store.search("redis rate limiting", 5);
|
|
@@ -157,25 +146,6 @@ async function testBackendSearchPropagatesErrors(): Promise<void> {
|
|
|
157
146
|
assert(message.includes("search unavailable"), "expected backend failures to propagate instead of falling back locally");
|
|
158
147
|
}
|
|
159
148
|
|
|
160
|
-
function testCjkScoring(): void {
|
|
161
|
-
const billing = memory({
|
|
162
|
-
issueNumber: 3,
|
|
163
|
-
title: "Memory: 账单修复流程",
|
|
164
|
-
detail: "遇到账单不一致时,先核对 invoice_id,再补发 webhook。",
|
|
165
|
-
topics: ["账单", "支付"],
|
|
166
|
-
});
|
|
167
|
-
const unrelated = memory({
|
|
168
|
-
issueNumber: 4,
|
|
169
|
-
title: "Memory: 部署备注",
|
|
170
|
-
detail: "发布前需要确认灰度流量比例。",
|
|
171
|
-
topics: ["部署"],
|
|
172
|
-
});
|
|
173
|
-
const billingScore = scoreMemoryMatch(billing, "账单 webhook");
|
|
174
|
-
const unrelatedScore = scoreMemoryMatch(unrelated, "账单 webhook");
|
|
175
|
-
assert(billingScore > unrelatedScore, "expected Chinese query scoring to prefer the billing memory");
|
|
176
|
-
assert(billingScore > 0, "expected Chinese query to produce a positive match score");
|
|
177
|
-
}
|
|
178
|
-
|
|
179
149
|
function testMergeMemoryCandidates(): void {
|
|
180
150
|
const merged = mergeMemoryCandidates(
|
|
181
151
|
[
|
|
@@ -207,6 +177,8 @@ async function testStructuredStoreAndSchema(): Promise<void> {
|
|
|
207
177
|
const ensured: string[][] = [];
|
|
208
178
|
const labels: LabelRecord[] = [{ name: "kind:lesson" }, { name: "topic:redis" }];
|
|
209
179
|
const client = {
|
|
180
|
+
repo: () => "owner/main-memory",
|
|
181
|
+
searchIssues: async () => [] as IssueRecord[],
|
|
210
182
|
listIssues: async () => [] as IssueRecord[],
|
|
211
183
|
listLabels: async () => labels,
|
|
212
184
|
ensureLabels: async (next: string[]) => { ensured.push(next); },
|
|
@@ -215,7 +187,7 @@ async function testStructuredStoreAndSchema(): Promise<void> {
|
|
|
215
187
|
return { number: 99, title: payload.title };
|
|
216
188
|
},
|
|
217
189
|
};
|
|
218
|
-
const store = new MemoryStore(client as never
|
|
190
|
+
const store = new MemoryStore(client as never);
|
|
219
191
|
const result = await store.store({ detail: "Redis Lua scripts are required for atomic rate limiting.", kind: "Lesson", topics: ["Redis Ops", "rate_limit"] });
|
|
220
192
|
const schema = await store.listSchema();
|
|
221
193
|
|
|
@@ -236,6 +208,47 @@ async function testStructuredStoreAndSchema(): Promise<void> {
|
|
|
236
208
|
assert(schema.topics.includes("redis"), "expected schema to expose existing topic labels");
|
|
237
209
|
}
|
|
238
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
|
+
|
|
239
252
|
async function testStoreKeepsFullAutoTitleAndSupportsExplicitTitle(): Promise<void> {
|
|
240
253
|
const created: Array<{ title: string; body: string; labels: string[] }> = [];
|
|
241
254
|
const client = {
|
|
@@ -247,7 +260,7 @@ async function testStoreKeepsFullAutoTitleAndSupportsExplicitTitle(): Promise<vo
|
|
|
247
260
|
return { number: created.length + 100, title: payload.title };
|
|
248
261
|
},
|
|
249
262
|
};
|
|
250
|
-
const store = new MemoryStore(client as never
|
|
263
|
+
const store = new MemoryStore(client as never);
|
|
251
264
|
const longDetail = "Tech Decision #001: Frontend = React Native, Backend = FastAPI, Database = PostgreSQL, and analytics events must stay append-only for auditability.";
|
|
252
265
|
const auto = await store.store({ detail: longDetail });
|
|
253
266
|
const explicit = await store.store({ title: "Architecture Decision #001", detail: "Use React Native + FastAPI for the first mobile stack." });
|
|
@@ -284,6 +297,11 @@ async function testGetAndListMemories(): Promise<void> {
|
|
|
284
297
|
})),
|
|
285
298
|
];
|
|
286
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
|
+
},
|
|
287
305
|
listIssues: async (params?: { labels?: string[]; state?: "open" | "closed" | "all" }) => {
|
|
288
306
|
const labels = params?.labels ?? [];
|
|
289
307
|
const state = params?.state ?? "open";
|
|
@@ -295,7 +313,7 @@ async function testGetAndListMemories(): Promise<void> {
|
|
|
295
313
|
});
|
|
296
314
|
},
|
|
297
315
|
};
|
|
298
|
-
const store = new MemoryStore(client as never
|
|
316
|
+
const store = new MemoryStore(client as never);
|
|
299
317
|
const exact = await store.get("4");
|
|
300
318
|
const activeFacts = await store.listMemories({ status: "active", kind: "core-fact", limit: 10 });
|
|
301
319
|
const sports = await store.listMemories({ status: "all", topic: "sports", limit: 10 });
|
|
@@ -317,6 +335,11 @@ async function testLegacyMemoriesWithoutSessionOrDate(): Promise<void> {
|
|
|
317
335
|
];
|
|
318
336
|
const client = {
|
|
319
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
|
+
},
|
|
320
343
|
listIssues: async (params?: { labels?: string[]; state?: "open" | "closed" | "all" }) => {
|
|
321
344
|
const labels = params?.labels ?? [];
|
|
322
345
|
const state = params?.state ?? "open";
|
|
@@ -329,7 +352,7 @@ async function testLegacyMemoriesWithoutSessionOrDate(): Promise<void> {
|
|
|
329
352
|
},
|
|
330
353
|
searchIssues: async () => issues,
|
|
331
354
|
};
|
|
332
|
-
const store = new MemoryStore(client as never
|
|
355
|
+
const store = new MemoryStore(client as never);
|
|
333
356
|
const exact = await store.get("4");
|
|
334
357
|
const recalled = await store.search("F1 Dota 2", 5);
|
|
335
358
|
|
|
@@ -352,6 +375,11 @@ async function testUpdateMemoryInPlace(): Promise<void> {
|
|
|
352
375
|
const updatedIssues: Array<{ number: number; title?: string; body?: string }> = [];
|
|
353
376
|
const syncedLabels: Array<{ number: number; labels: string[] }> = [];
|
|
354
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
|
+
},
|
|
355
383
|
listIssues: async (params?: { labels?: string[]; state?: "open" | "closed" | "all" }) => {
|
|
356
384
|
const labels = params?.labels ?? [];
|
|
357
385
|
const state = params?.state ?? "open";
|
|
@@ -378,7 +406,7 @@ async function testUpdateMemoryInPlace(): Promise<void> {
|
|
|
378
406
|
issue.labels = labels;
|
|
379
407
|
},
|
|
380
408
|
};
|
|
381
|
-
const store = new MemoryStore(client as never
|
|
409
|
+
const store = new MemoryStore(client as never);
|
|
382
410
|
const updated = await store.update("4", {
|
|
383
411
|
detail: "xiangz likes F1, watches Dota 2 as a viewer, and recently follows tennis.",
|
|
384
412
|
topics: ["preferences", "sports"],
|
|
@@ -407,6 +435,11 @@ async function testUpdateSupportsExplicitRetitle(): Promise<void> {
|
|
|
407
435
|
];
|
|
408
436
|
const updatedIssues: Array<{ number: number; title?: string; body?: string }> = [];
|
|
409
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
|
+
},
|
|
410
443
|
listIssues: async (params?: { labels?: string[]; state?: "open" | "closed" | "all" }) => {
|
|
411
444
|
const labels = params?.labels ?? [];
|
|
412
445
|
const state = params?.state ?? "open";
|
|
@@ -428,13 +461,58 @@ async function testUpdateSupportsExplicitRetitle(): Promise<void> {
|
|
|
428
461
|
},
|
|
429
462
|
syncManagedLabels: async () => {},
|
|
430
463
|
};
|
|
431
|
-
const store = new MemoryStore(client as never
|
|
464
|
+
const store = new MemoryStore(client as never);
|
|
432
465
|
const updated = await store.update("20", { title: "Billing Audit Convention" });
|
|
433
466
|
|
|
434
467
|
assert(updated?.title === "Memory: Billing Audit Convention", "expected memory_update to support explicit retitle");
|
|
435
468
|
assert(updatedIssues[0]?.title === "Memory: Billing Audit Convention", "expected issue title patch to use the explicit retitle");
|
|
436
469
|
}
|
|
437
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
|
+
|
|
438
516
|
async function testForgetClosesMemoryIssue(): Promise<void> {
|
|
439
517
|
const issues: IssueRecord[] = [
|
|
440
518
|
issueFromMemory(memory({
|
|
@@ -448,6 +526,11 @@ async function testForgetClosesMemoryIssue(): Promise<void> {
|
|
|
448
526
|
const syncedLabels: Array<{ number: number; labels: string[] }> = [];
|
|
449
527
|
const updatedIssues: Array<{ number: number; state?: "open" | "closed" }> = [];
|
|
450
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
|
+
},
|
|
451
534
|
listIssues: async (params?: { labels?: string[]; state?: "open" | "closed" | "all" }) => {
|
|
452
535
|
const labels = params?.labels ?? [];
|
|
453
536
|
const state = params?.state ?? "open";
|
|
@@ -472,7 +555,7 @@ async function testForgetClosesMemoryIssue(): Promise<void> {
|
|
|
472
555
|
return issue;
|
|
473
556
|
},
|
|
474
557
|
};
|
|
475
|
-
const store = new MemoryStore(client as never
|
|
558
|
+
const store = new MemoryStore(client as never);
|
|
476
559
|
const forgotten = await store.forget("12");
|
|
477
560
|
|
|
478
561
|
assert(forgotten?.status === "stale", "expected forgotten memory to be returned as stale");
|
|
@@ -485,15 +568,17 @@ async function main(): Promise<void> {
|
|
|
485
568
|
await testBackendSearchPreferredForRecall();
|
|
486
569
|
await testBackendSearchReturnsEmptyWithoutLexicalFallback();
|
|
487
570
|
await testBackendSearchPropagatesErrors();
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
await
|
|
571
|
+
testMergeMemoryCandidates();
|
|
572
|
+
await testStructuredStoreAndSchema();
|
|
573
|
+
await testListSchemaPrefersLabelsWithoutIssueScan();
|
|
574
|
+
await testStoreDeduplicatesViaHashSearch();
|
|
491
575
|
await testStoreKeepsFullAutoTitleAndSupportsExplicitTitle();
|
|
492
576
|
await testGetAndListMemories();
|
|
493
577
|
await testLegacyMemoriesWithoutSessionOrDate();
|
|
494
578
|
await testUpdateMemoryInPlace();
|
|
495
579
|
await testUpdateSupportsExplicitRetitle();
|
|
496
580
|
await testForgetClosesMemoryIssue();
|
|
581
|
+
await testUpdateUsesHashSearchForDuplicateCheck();
|
|
497
582
|
console.log("memory tests passed");
|
|
498
583
|
}
|
|
499
584
|
|