@clawmem-ai/clawmem 0.1.13 → 0.1.15
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 -4
- package/openclaw.plugin.json +2 -2
- package/package.json +1 -1
- package/skills/clawmem/SKILL.md +14 -7
- package/skills/clawmem/references/manual-ops.md +1 -0
- package/skills/clawmem/references/schema.md +2 -0
- package/src/config.test.ts +1 -1
- package/src/config.ts +1 -1
- package/src/memory.test.ts +130 -37
- package/src/memory.ts +83 -34
- package/src/recall-sanitize.ts +143 -0
- package/src/service.test.ts +149 -0
- package/src/service.ts +236 -16
- package/src/types.ts +1 -1
package/README.md
CHANGED
|
@@ -138,7 +138,7 @@ Full config with all options:
|
|
|
138
138
|
turnCommentDelayMs: 1000,
|
|
139
139
|
summaryWaitTimeoutMs: 120000,
|
|
140
140
|
memoryRecallLimit: 5,
|
|
141
|
-
memoryAutoRecallLimit:
|
|
141
|
+
memoryAutoRecallLimit: 3
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
}
|
|
@@ -152,8 +152,9 @@ Full config with all options:
|
|
|
152
152
|
|
|
153
153
|
- Conversation comments exclude tool calls, tool results, system messages, and heartbeat noise.
|
|
154
154
|
- Summary failures do not block finalization; the `summary` field is written as `failed: ...`.
|
|
155
|
-
- Memory search and auto-
|
|
156
|
-
-
|
|
155
|
+
- Memory search and auto-recall only return open `type:memory` issues. Closed memory issues are treated as stale.
|
|
156
|
+
- ClawMem automatically injects a small set of relevant memories before each turn using the agent's default repo and the backend recall API. Auto-recall is best-effort and quietly skips injection when backend recall is unavailable.
|
|
157
|
+
- `memory_recall` uses the backend `/api/v3/search/issues` endpoint scoped to the current repo plus `label:"type:memory"`. When backend recall is unavailable, use `memory_list` or `memory_get` to inspect memories explicitly.
|
|
157
158
|
- Durable memories are extracted best-effort during later request-scoped maintenance, not by background subagent work after a request has already ended.
|
|
158
159
|
- The plugin exposes `memory_repos`, `memory_repo_create`, `memory_list`, `memory_get`, `memory_labels`, `memory_recall`, `memory_store`, `memory_update`, and `memory_forget` for mid-session use.
|
|
159
160
|
- Route resolution is now: agent identity supplies credentials, `defaultRepo` is the fallback memory space, and explicit tool calls may override repo per operation.
|
|
@@ -162,4 +163,4 @@ Full config with all options:
|
|
|
162
163
|
- `memory_update` updates one existing memory issue in place; use it for evolving canonical facts or active tasks instead of creating a duplicate node.
|
|
163
164
|
- Conversation lifecycle is stored in native issue state (`open` while live, `closed` after finalize); memory lifecycle uses native issue state too (`open` active, `closed` stale).
|
|
164
165
|
- Memory extraction now prefers one atomic fact per memory item instead of bundling whole sessions into a single node.
|
|
165
|
-
- Memory issue bodies store the durable detail plus flat metadata such as `memory_hash` and logical `date`;
|
|
166
|
+
- Memory issue bodies store the durable detail in a YAML `detail` field plus flat metadata such as `memory_hash` and logical `date`; this matches the current Console parser in `agent-git-service/web`.
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "clawmem",
|
|
3
3
|
"name": "ClawMem",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.15",
|
|
5
5
|
"description": "Mirror OpenClaw sessions into GitHub-compatible issues and comments.",
|
|
6
6
|
"kind": "memory",
|
|
7
7
|
"skills": [
|
|
@@ -127,7 +127,7 @@
|
|
|
127
127
|
},
|
|
128
128
|
"memoryAutoRecallLimit": {
|
|
129
129
|
"label": "Auto Recall Limit",
|
|
130
|
-
"help": "Maximum number of active memories injected
|
|
130
|
+
"help": "Maximum number of active memories automatically injected before each turn."
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
}
|
package/package.json
CHANGED
package/skills/clawmem/SKILL.md
CHANGED
|
@@ -25,20 +25,22 @@ Memory hygiene matters: lock important insights deliberately, update canonical f
|
|
|
25
25
|
The ClawMem plugin automatically handles:
|
|
26
26
|
- Per-agent provisioning of credentials plus a default memory repo
|
|
27
27
|
- Session mirroring into `type:conversation` issues
|
|
28
|
+
- Best-effort automatic memory recall before each turn, scoped to the current agent's `defaultRepo`
|
|
28
29
|
- Best-effort durable memory extraction during later request-scoped maintenance
|
|
29
|
-
- Automatic recall of relevant active memories at session start
|
|
30
30
|
- Mid-session memory tools: `memory_repos`, `memory_repo_create`, `memory_list`, `memory_get`, `memory_labels`, `memory_recall`, `memory_store`, `memory_update`, and `memory_forget`
|
|
31
31
|
|
|
32
|
-
Automatic recall is only a bootstrap. You still need to retrieve before answering when memory may matter, and save after learning something durable.
|
|
33
|
-
|
|
34
32
|
## Mandatory turn loop
|
|
35
33
|
|
|
36
34
|
On every user turn, run this loop:
|
|
37
35
|
|
|
38
36
|
1. Before answering, ask: could ClawMem improve this answer?
|
|
39
37
|
- Default to yes for user preferences, project history, prior decisions, lessons, conventions, terminology, recurring problems, and active tasks.
|
|
40
|
-
-
|
|
41
|
-
-
|
|
38
|
+
- Auto-recall may already inject useful context from the current agent's `defaultRepo`, but it is only a hint. Do not treat missing auto-recall context as proof that no relevant memory exists.
|
|
39
|
+
- If the injected context already answers the question, you do not need to immediately call `memory_recall` again.
|
|
40
|
+
- Auto-recall does not currently fan out across every accessible repo. Shared organization memory, team memory, and project memory outside the current `defaultRepo` will not be recalled automatically.
|
|
41
|
+
- Before explicit memory work, choose the right repo. If unclear, inspect `memory_repos` and fall back to the agent's `defaultRepo`. If the likely memory lives outside the default repo, use explicit repo selection instead of relying on auto-recall.
|
|
42
|
+
- Use `memory_recall` when injected context is missing, weak, cross-repo, high-stakes, or when you need an explicit retrieval trace.
|
|
43
|
+
- Write `memory_recall.query` as a short natural-language intent. Do not paste long code blocks, full logs, tool chatter, or system prompt text unless the exact wording is necessary.
|
|
42
44
|
- When the question spans more than one angle, run more than one recall query across keywords, topics, synonyms, and likely project phrasing.
|
|
43
45
|
- If `memory_recall` is weak or empty and the answer depends on whether a memory exists, cross-check with `memory_list`.
|
|
44
46
|
- If the first recall pass is weak, broaden with shorter terms, adjacent topics, or alternate phrasing before concluding a miss.
|
|
@@ -50,14 +52,16 @@ On every user turn, run this loop:
|
|
|
50
52
|
- Use `memory_update` when the same canonical fact or ongoing task should keep evolving as one node.
|
|
51
53
|
- When updating an existing memory, preserve that node's current language unless the user explicitly asks for a rewrite.
|
|
52
54
|
- Use `memory_store` when this is a genuinely new memory.
|
|
55
|
+
- When using `memory_store`, pass both `title` and `detail` when you can. Keep the title concise and human-readable, and keep `detail` as the full durable fact.
|
|
56
|
+
- When using `memory_update`, pass `title` as well if the existing title is too short, outdated, or less precise than the current canonical fact.
|
|
53
57
|
- For new memories, write the memory title and body in the user's current language by default.
|
|
54
58
|
- Use `memory_forget` when a memory is stale, superseded, or harmful if reused.
|
|
55
59
|
3. Keep the user posted.
|
|
56
|
-
- If a retrieved memory materially shaped the answer,
|
|
60
|
+
- If a retrieved memory materially shaped the answer, briefly surface that fact in the user's current language.
|
|
57
61
|
- Include the memory id and title only when they help with debugging, traceability, or an explicit user request.
|
|
58
62
|
- After creating or updating a memory, give a short confirmation in the user's current language instead of forcing fixed English phrasing.
|
|
59
63
|
|
|
60
|
-
Bias toward
|
|
64
|
+
Bias toward saving, and use explicit retrieval whenever auto-recall is absent, weak, cross-repo, or too ambiguous to trust on its own.
|
|
61
65
|
|
|
62
66
|
## Retrieval and storage rules
|
|
63
67
|
|
|
@@ -67,7 +71,10 @@ Bias toward retrieving and saving. A missed search or missed memory is worse tha
|
|
|
67
71
|
- Private personal memory usually belongs in the agent's `defaultRepo`.
|
|
68
72
|
- Project memory belongs in the relevant project repo.
|
|
69
73
|
- Shared or team knowledge belongs in the shared repo for that group.
|
|
74
|
+
- Shared or team knowledge in another repo is not part of default auto-recall today. To use it, select that repo explicitly with `memory_recall`, `memory_list`, or `memory_get`.
|
|
70
75
|
- Memory titles and bodies default to the user's current language for new memories.
|
|
76
|
+
- Prefer a short standalone title plus a fuller `detail` body instead of stuffing the whole memory into the title.
|
|
77
|
+
- If you omit `title`, the plugin may derive it from `detail`, but providing an explicit title is preferred for readability in the Console.
|
|
71
78
|
- When updating an existing memory, keep that node in its current language unless the user explicitly asks to rewrite it.
|
|
72
79
|
- Keep schema labels and machine-oriented fields stable. Do not translate `type:*`, `kind:*`, `topic:*`, or other structural identifiers.
|
|
73
80
|
- If the user is asking about collaboration, organizations, teams, invitations, collaborators, shared repo access, or why someone can or cannot access a memory repo, switch from normal memory reasoning to the collaboration workflow in `references/collaboration.md`.
|
|
@@ -76,6 +76,7 @@ Do not export `GH_HOST` or `GH_ENTERPRISE_TOKEN` globally for unrelated github.c
|
|
|
76
76
|
Use the tool path first. If raw issue control is required:
|
|
77
77
|
|
|
78
78
|
- For new memories, write the issue title and body in the user's current language.
|
|
79
|
+
- Prefer a concise standalone title and keep the full durable fact in the body.
|
|
79
80
|
- When manually updating an existing memory, preserve that memory's current language unless the user explicitly asks for a rewrite.
|
|
80
81
|
- Keep labels and schema markers such as `type:*`, `kind:*`, and `topic:*` in their fixed machine-readable form.
|
|
81
82
|
|
|
@@ -61,6 +61,8 @@ If you create a curated memory manually, include:
|
|
|
61
61
|
## Storage language
|
|
62
62
|
|
|
63
63
|
- For new memory nodes, write the human-readable title and body in the user's current language by default.
|
|
64
|
+
- When using plugin tools, prefer passing an explicit short `title` plus a fuller `detail` body.
|
|
65
|
+
- Do not treat the title as the only durable content. The body detail should still contain the full reusable fact.
|
|
64
66
|
- When updating an existing memory node, preserve that node's current language unless the user explicitly asks for a rewrite.
|
|
65
67
|
- Do not translate schema or routing markers such as `type:*`, `kind:*`, `topic:*`, or other machine-oriented field names.
|
|
66
68
|
|
package/src/config.test.ts
CHANGED
package/src/config.ts
CHANGED
|
@@ -46,7 +46,7 @@ export function resolvePluginConfig(api: OpenClawPluginApi): ClawMemPluginConfig
|
|
|
46
46
|
authScheme: raw.authScheme === "bearer" ? "bearer" : "token",
|
|
47
47
|
agents,
|
|
48
48
|
memoryRecallLimit: clamp(num(raw.memoryRecallLimit, 5), 1, 20),
|
|
49
|
-
memoryAutoRecallLimit: clamp(num(raw.memoryAutoRecallLimit,
|
|
49
|
+
memoryAutoRecallLimit: clamp(num(raw.memoryAutoRecallLimit, 3), 1, 20),
|
|
50
50
|
turnCommentDelayMs: num(raw.turnCommentDelayMs, 1000),
|
|
51
51
|
summaryWaitTimeoutMs: clamp(num(raw.summaryWaitTimeoutMs, 120000), 1000, 600000),
|
|
52
52
|
};
|
package/src/memory.test.ts
CHANGED
|
@@ -44,46 +44,53 @@ function assert(condition: unknown, message: string): void {
|
|
|
44
44
|
function testConfig(): never {
|
|
45
45
|
return {
|
|
46
46
|
memoryRecallLimit: 5,
|
|
47
|
-
memoryAutoRecallLimit:
|
|
47
|
+
memoryAutoRecallLimit: 3,
|
|
48
48
|
turnCommentDelayMs: 1000,
|
|
49
49
|
summaryWaitTimeoutMs: 120000,
|
|
50
50
|
} as never;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
async function
|
|
54
|
-
const
|
|
55
|
-
issueFromMemory(memory({
|
|
56
|
-
issueNumber: 1,
|
|
57
|
-
title: "Memory: Redis rate limit tuning",
|
|
58
|
-
detail: "Distributed Redis rate limiting must use Lua scripts to stay atomic.",
|
|
59
|
-
kind: "lesson",
|
|
60
|
-
topics: ["redis", "rate-limiting"],
|
|
61
|
-
})),
|
|
62
|
-
issueFromMemory(memory({
|
|
63
|
-
issueNumber: 2,
|
|
64
|
-
title: "Memory: Generic backend notes",
|
|
65
|
-
detail: "We use Redis in several services, but this one is not about rate limiting.",
|
|
66
|
-
topics: ["backend"],
|
|
67
|
-
})),
|
|
68
|
-
];
|
|
53
|
+
async function testBackendSearchBuildsSingleCleanedQuery(): Promise<void> {
|
|
54
|
+
const queries: string[] = [];
|
|
69
55
|
const client = {
|
|
70
|
-
|
|
56
|
+
repo: () => "owner/main-memory",
|
|
57
|
+
searchIssues: async (query: string) => {
|
|
58
|
+
queries.push(query);
|
|
59
|
+
return [] as IssueRecord[];
|
|
60
|
+
},
|
|
71
61
|
};
|
|
72
62
|
const store = new MemoryStore(client as never, {} as never, testConfig());
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
63
|
+
await store.search([
|
|
64
|
+
"<clawmem-context>",
|
|
65
|
+
"- [11] Previous memory that should be stripped",
|
|
66
|
+
"</clawmem-context>",
|
|
67
|
+
"Conversation info (untrusted metadata):",
|
|
68
|
+
"```json",
|
|
69
|
+
'{"channel":"slack"}',
|
|
70
|
+
"```",
|
|
71
|
+
"",
|
|
72
|
+
"[message_id: abc-123]",
|
|
73
|
+
"",
|
|
74
|
+
"[Slack 2026-04-03 09:30]: Please help debug the Redis rate limiting path.",
|
|
75
|
+
"See https://example.com/debug for more context.",
|
|
76
|
+
"throw new TimeoutError('lua script timeout')",
|
|
77
|
+
"[System: auto-translated]",
|
|
78
|
+
].join("\n"), 5);
|
|
79
|
+
|
|
80
|
+
assert(queries.length === 1, "expected a single backend search query");
|
|
81
|
+
assert(queries[0]?.includes("repo:owner/main-memory"), "expected the backend query to stay scoped to the repo");
|
|
82
|
+
assert(queries[0]?.includes('label:"type:memory"'), "expected the backend query to filter memory issues");
|
|
83
|
+
assert((queries[0] ?? "").length <= 1610, "expected the backend search query to stay within the configured cap plus qualifiers");
|
|
84
|
+
assert(queries[0]?.toLowerCase().includes("redis"), "expected the backend query to retain key terms");
|
|
85
|
+
assert(!queries[0]?.includes("<clawmem-context>"), "expected injected clawmem context to be stripped");
|
|
86
|
+
assert(!queries[0]?.includes("https://example.com/debug"), "expected URLs to be stripped from backend recall");
|
|
87
|
+
assert(!queries[0]?.includes("Conversation info (untrusted metadata):"), "expected inbound metadata blocks to be stripped");
|
|
88
|
+
assert(!queries[0]?.includes("[message_id:"), "expected message id hints to be stripped");
|
|
89
|
+
assert(!queries[0]?.includes("[Slack 2026-04-03 09:30]"), "expected envelope prefixes to be stripped");
|
|
90
|
+
assert(!queries[0]?.includes("[System: auto-translated]"), "expected trailing system hints to be stripped");
|
|
76
91
|
}
|
|
77
92
|
|
|
78
93
|
async function testBackendSearchPreferredForRecall(): Promise<void> {
|
|
79
|
-
const listed = [
|
|
80
|
-
issueFromMemory(memory({
|
|
81
|
-
issueNumber: 1,
|
|
82
|
-
title: "Memory: lexical decoy",
|
|
83
|
-
detail: "redis rate limiting checklist",
|
|
84
|
-
kind: "lesson",
|
|
85
|
-
})),
|
|
86
|
-
];
|
|
87
94
|
const searched = [
|
|
88
95
|
issueFromMemory(memory({
|
|
89
96
|
issueNumber: 2,
|
|
@@ -96,14 +103,13 @@ async function testBackendSearchPreferredForRecall(): Promise<void> {
|
|
|
96
103
|
const queries: string[] = [];
|
|
97
104
|
const client = {
|
|
98
105
|
repo: () => "owner/main-memory",
|
|
99
|
-
listIssues: async () => listed,
|
|
100
106
|
searchIssues: async (query: string) => {
|
|
101
107
|
queries.push(query);
|
|
102
108
|
return searched;
|
|
103
109
|
},
|
|
104
110
|
};
|
|
105
111
|
const store = new MemoryStore(client as never, {} as never, testConfig());
|
|
106
|
-
const found = await store.search("redis rate limiting",
|
|
112
|
+
const found = await store.search("redis rate limiting", 1);
|
|
107
113
|
|
|
108
114
|
assert(queries.length === 1, "expected backend search to be called once");
|
|
109
115
|
assert(queries[0]?.includes('repo:owner/main-memory'), "expected backend query to scope to the current repo");
|
|
@@ -111,7 +117,7 @@ async function testBackendSearchPreferredForRecall(): Promise<void> {
|
|
|
111
117
|
assert(found.length === 1 && found[0]?.issueNumber === 2, "expected backend search results to be preferred");
|
|
112
118
|
}
|
|
113
119
|
|
|
114
|
-
async function
|
|
120
|
+
async function testBackendSearchReturnsEmptyWithoutLexicalFallback(): Promise<void> {
|
|
115
121
|
const issues = [
|
|
116
122
|
issueFromMemory(memory({
|
|
117
123
|
issueNumber: 3,
|
|
@@ -124,12 +130,28 @@ async function testBackendSearchFallsBackToLocalLexical(): Promise<void> {
|
|
|
124
130
|
const client = {
|
|
125
131
|
repo: () => "owner/main-memory",
|
|
126
132
|
listIssues: async () => issues,
|
|
127
|
-
searchIssues: async () =>
|
|
133
|
+
searchIssues: async () => [] as IssueRecord[],
|
|
128
134
|
};
|
|
129
|
-
const store = new MemoryStore(client as never, {
|
|
135
|
+
const store = new MemoryStore(client as never, {} as never, testConfig());
|
|
130
136
|
const found = await store.search("redis rate limiting", 5);
|
|
131
137
|
|
|
132
|
-
assert(found.length ===
|
|
138
|
+
assert(found.length === 0, "expected backend-only recall to return no results when the backend finds nothing");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function testBackendSearchPropagatesErrors(): Promise<void> {
|
|
142
|
+
const client = {
|
|
143
|
+
repo: () => "owner/main-memory",
|
|
144
|
+
searchIssues: async () => { throw new Error("search unavailable"); },
|
|
145
|
+
};
|
|
146
|
+
const store = new MemoryStore(client as never, {} as never, testConfig());
|
|
147
|
+
let message = "";
|
|
148
|
+
try {
|
|
149
|
+
await store.search("redis rate limiting", 5);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
message = String(error);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
assert(message.includes("search unavailable"), "expected backend failures to propagate instead of falling back locally");
|
|
133
155
|
}
|
|
134
156
|
|
|
135
157
|
function testCjkScoring(): void {
|
|
@@ -177,12 +199,36 @@ async function testStructuredStoreAndSchema(): Promise<void> {
|
|
|
177
199
|
assert(created[0]?.labels.includes("topic:rate-limit"), "expected created labels to include normalized topic");
|
|
178
200
|
assert(!created[0]?.labels.some((label) => label.startsWith("session:")), "expected manual memory_store writes to omit synthetic session labels");
|
|
179
201
|
assert(!created[0]?.labels.some((label) => label.startsWith("date:")), "expected new memory labels to omit date labels");
|
|
202
|
+
assert(created[0]?.body.includes("memory_hash:"), "expected new memory body to retain metadata fields");
|
|
203
|
+
assert(created[0]?.body.includes("detail: Redis Lua scripts are required for atomic rate limiting."), "expected new memory body to store detail in YAML");
|
|
180
204
|
assert(created[0]?.body.includes(`date: ${result.memory.date}`), "expected new memory body to retain logical date metadata");
|
|
181
205
|
assert(ensured[0]?.includes("kind:lesson"), "expected ensureLabels to include kind label");
|
|
182
206
|
assert(schema.kinds.includes("lesson"), "expected schema to expose existing kind labels");
|
|
183
207
|
assert(schema.topics.includes("redis"), "expected schema to expose existing topic labels");
|
|
184
208
|
}
|
|
185
209
|
|
|
210
|
+
async function testStoreKeepsFullAutoTitleAndSupportsExplicitTitle(): Promise<void> {
|
|
211
|
+
const created: Array<{ title: string; body: string; labels: string[] }> = [];
|
|
212
|
+
const client = {
|
|
213
|
+
listIssues: async () => [] as IssueRecord[],
|
|
214
|
+
listLabels: async () => [] as LabelRecord[],
|
|
215
|
+
ensureLabels: async () => {},
|
|
216
|
+
createIssue: async (payload: { title: string; body: string; labels: string[] }) => {
|
|
217
|
+
created.push(payload);
|
|
218
|
+
return { number: created.length + 100, title: payload.title };
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
const store = new MemoryStore(client as never, {} as never, testConfig());
|
|
222
|
+
const longDetail = "Tech Decision #001: Frontend = React Native, Backend = FastAPI, Database = PostgreSQL, and analytics events must stay append-only for auditability.";
|
|
223
|
+
const auto = await store.store({ detail: longDetail });
|
|
224
|
+
const explicit = await store.store({ title: "Architecture Decision #001", detail: "Use React Native + FastAPI for the first mobile stack." });
|
|
225
|
+
|
|
226
|
+
assert(auto.memory.title === `Memory: ${longDetail}`, "expected auto-generated memory title to keep the full detail without truncation");
|
|
227
|
+
assert(explicit.memory.title === "Memory: Architecture Decision #001", "expected explicit memory title to be preserved");
|
|
228
|
+
assert(created[0]?.title === `Memory: ${longDetail}`, "expected created issue title to keep the full auto title");
|
|
229
|
+
assert(created[1]?.title === "Memory: Architecture Decision #001", "expected created issue title to use the explicit title");
|
|
230
|
+
}
|
|
231
|
+
|
|
186
232
|
async function testGetAndListMemories(): Promise<void> {
|
|
187
233
|
const issues = [
|
|
188
234
|
issueFromMemory(memory({
|
|
@@ -241,6 +287,7 @@ async function testLegacyMemoriesWithoutSessionOrDate(): Promise<void> {
|
|
|
241
287
|
},
|
|
242
288
|
];
|
|
243
289
|
const client = {
|
|
290
|
+
repo: () => "owner/main-memory",
|
|
244
291
|
listIssues: async (params?: { labels?: string[]; state?: "open" | "closed" | "all" }) => {
|
|
245
292
|
const labels = params?.labels ?? [];
|
|
246
293
|
const state = params?.state ?? "open";
|
|
@@ -251,6 +298,7 @@ async function testLegacyMemoriesWithoutSessionOrDate(): Promise<void> {
|
|
|
251
298
|
return (issue.state ?? "open") === state;
|
|
252
299
|
});
|
|
253
300
|
},
|
|
301
|
+
searchIssues: async () => issues,
|
|
254
302
|
};
|
|
255
303
|
const store = new MemoryStore(client as never, {} as never, testConfig());
|
|
256
304
|
const exact = await store.get("4");
|
|
@@ -312,10 +360,52 @@ async function testUpdateMemoryInPlace(): Promise<void> {
|
|
|
312
360
|
assert(JSON.stringify(updated?.topics) === JSON.stringify(["preferences", "sports"]), "expected topics to be replaced");
|
|
313
361
|
assert(updatedIssues.length === 1, "expected a single issue update");
|
|
314
362
|
assert(updatedIssues[0]?.title !== "Memory: xiangz preferences", "expected title to refresh from updated detail");
|
|
363
|
+
assert(updatedIssues[0]?.body?.includes("memory_hash:"), "expected updated body to retain metadata");
|
|
364
|
+
assert(updatedIssues[0]?.body?.includes("detail:"), "expected updated body to store a detail field in YAML");
|
|
365
|
+
assert(updatedIssues[0]?.body?.includes("recently follows tennis"), "expected updated body to contain the updated detail text");
|
|
315
366
|
assert(ensured[0]?.includes("topic:sports"), "expected new topic label to be ensured");
|
|
316
367
|
assert(syncedLabels[0]?.labels.includes("kind:core-fact"), "expected existing kind label to be preserved");
|
|
317
368
|
}
|
|
318
369
|
|
|
370
|
+
async function testUpdateSupportsExplicitRetitle(): Promise<void> {
|
|
371
|
+
const issues: IssueRecord[] = [
|
|
372
|
+
issueFromMemory(memory({
|
|
373
|
+
issueNumber: 20,
|
|
374
|
+
title: "Memory: old short title",
|
|
375
|
+
detail: "We use append-only audit events for billing changes.",
|
|
376
|
+
kind: "convention",
|
|
377
|
+
})),
|
|
378
|
+
];
|
|
379
|
+
const updatedIssues: Array<{ number: number; title?: string; body?: string }> = [];
|
|
380
|
+
const client = {
|
|
381
|
+
listIssues: async (params?: { labels?: string[]; state?: "open" | "closed" | "all" }) => {
|
|
382
|
+
const labels = params?.labels ?? [];
|
|
383
|
+
const state = params?.state ?? "open";
|
|
384
|
+
return issues.filter((issue) => {
|
|
385
|
+
const issueLabels = issue.labels ?? [];
|
|
386
|
+
if (!labels.every((label) => issueLabels.includes(label))) return false;
|
|
387
|
+
if (state === "all") return true;
|
|
388
|
+
return (issue.state ?? "open") === state;
|
|
389
|
+
});
|
|
390
|
+
},
|
|
391
|
+
ensureLabels: async () => {},
|
|
392
|
+
updateIssue: async (number: number, patch: { title?: string; body?: string }) => {
|
|
393
|
+
updatedIssues.push({ number, ...patch });
|
|
394
|
+
const issue = issues.find((entry) => entry.number === number);
|
|
395
|
+
if (!issue) throw new Error("issue missing");
|
|
396
|
+
if (patch.title) issue.title = patch.title;
|
|
397
|
+
if (patch.body) issue.body = patch.body;
|
|
398
|
+
return issue;
|
|
399
|
+
},
|
|
400
|
+
syncManagedLabels: async () => {},
|
|
401
|
+
};
|
|
402
|
+
const store = new MemoryStore(client as never, {} as never, testConfig());
|
|
403
|
+
const updated = await store.update("20", { title: "Billing Audit Convention" });
|
|
404
|
+
|
|
405
|
+
assert(updated?.title === "Memory: Billing Audit Convention", "expected memory_update to support explicit retitle");
|
|
406
|
+
assert(updatedIssues[0]?.title === "Memory: Billing Audit Convention", "expected issue title patch to use the explicit retitle");
|
|
407
|
+
}
|
|
408
|
+
|
|
319
409
|
async function testForgetClosesMemoryIssue(): Promise<void> {
|
|
320
410
|
const issues: IssueRecord[] = [
|
|
321
411
|
issueFromMemory(memory({
|
|
@@ -362,14 +452,17 @@ async function testForgetClosesMemoryIssue(): Promise<void> {
|
|
|
362
452
|
}
|
|
363
453
|
|
|
364
454
|
async function main(): Promise<void> {
|
|
365
|
-
await
|
|
455
|
+
await testBackendSearchBuildsSingleCleanedQuery();
|
|
366
456
|
await testBackendSearchPreferredForRecall();
|
|
367
|
-
await
|
|
457
|
+
await testBackendSearchReturnsEmptyWithoutLexicalFallback();
|
|
458
|
+
await testBackendSearchPropagatesErrors();
|
|
368
459
|
testCjkScoring();
|
|
369
460
|
await testStructuredStoreAndSchema();
|
|
461
|
+
await testStoreKeepsFullAutoTitleAndSupportsExplicitTitle();
|
|
370
462
|
await testGetAndListMemories();
|
|
371
463
|
await testLegacyMemoriesWithoutSessionOrDate();
|
|
372
464
|
await testUpdateMemoryInPlace();
|
|
465
|
+
await testUpdateSupportsExplicitRetitle();
|
|
373
466
|
await testForgetClosesMemoryIssue();
|
|
374
467
|
console.log("memory tests passed");
|
|
375
468
|
}
|