@clawmem-ai/clawmem 0.1.7 → 0.1.9

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/src/service.ts CHANGED
@@ -30,16 +30,18 @@ class ClawMemService {
30
30
  }
31
31
 
32
32
  register(): void {
33
- this.api.on("before_agent_start", async (ev, ctx) => this.handleRecall(ev.prompt, ctx.agentId));
33
+ this.api.on("before_agent_start", async (ev, ctx) => this.handleBeforeAgentStart(ev.prompt, ctx.agentId));
34
34
  this.api.on("agent_end", (ev, ctx) => this.scheduleTurn({ sessionId: ctx.sessionId, sessionKey: ctx.sessionKey, agentId: ctx.agentId, messages: ev.messages }));
35
35
  this.api.on("before_reset", (ev, ctx) => this.enqueueFinalize({ sessionId: ctx.sessionId, sessionKey: ctx.sessionKey, sessionFile: ev.sessionFile, agentId: ctx.agentId, reason: ev.reason, messages: ev.messages }));
36
36
  this.api.on("session_end", (ev, ctx) => this.enqueueFinalize({ sessionId: ev.sessionId ?? ctx.sessionId, sessionKey: ev.sessionKey ?? ctx.sessionKey, agentId: ctx.agentId, reason: "session_end" }));
37
+ this.registerTools();
37
38
 
38
39
  this.api.registerService({
39
40
  id: "clawmem",
40
- start: async (ctx) => {
41
+ start: async (ctx: { stateDir: string }) => {
41
42
  this.statePath = resolveStatePath(ctx.stateDir);
42
43
  await this.ensureLoaded();
44
+ this.warnIfInactiveMemorySlot();
43
45
  this.unsubTranscript = this.api.runtime.events.onSessionTranscriptUpdate((u) => {
44
46
  void this.track(this.handleTranscript(u.sessionFile)).catch((e) => this.warn("transcript update", e));
45
47
  });
@@ -61,10 +63,254 @@ class ClawMemService {
61
63
  });
62
64
  }
63
65
 
64
- private async handleRecall(prompt: unknown, agentId?: string): Promise<{ prependContext: string } | void> {
65
- if (typeof prompt !== "string" || prompt.trim().length < 5) return;
66
+ private registerTools(): void {
67
+ this.api.registerTool({
68
+ name: "memory_list",
69
+ description: "List ClawMem memories by status or schema so the agent can inspect the current memory index before deduping or saving.",
70
+ required: true,
71
+ parameters: {
72
+ type: "object",
73
+ additionalProperties: false,
74
+ properties: {
75
+ status: { type: "string", enum: ["active", "stale", "all"], description: "Which memories to list. Defaults to active." },
76
+ kind: { type: "string", minLength: 1, description: "Optional kind filter, for example core-fact, lesson, or task." },
77
+ topic: { type: "string", minLength: 1, description: "Optional topic filter." },
78
+ limit: { type: "integer", minimum: 1, maximum: 200, description: "Maximum number of memories to return." },
79
+ agentId: { type: "string", minLength: 1, description: "Optional agent route override. Defaults to the current agent when available." },
80
+ },
81
+ },
82
+ execute: async (_id: string, params: unknown) => {
83
+ const p = asRecord(params);
84
+ const agentId = this.resolveToolAgentId(p.agentId);
85
+ if (!(await this.ensureConfigured(agentId))) return toolText(`ClawMem route for agent "${agentId}" is not configured.`);
86
+ const { mem } = this.getServices(agentId);
87
+ const status = p.status === "stale" || p.status === "all" ? p.status : "active";
88
+ const limit = typeof p.limit === "number" && Number.isFinite(p.limit) ? Math.floor(p.limit) : 20;
89
+ const kind = typeof p.kind === "string" && p.kind.trim() ? p.kind.trim() : undefined;
90
+ const topic = typeof p.topic === "string" && p.topic.trim() ? p.topic.trim() : undefined;
91
+ const memories = await mem.listMemories({ status, kind, topic, limit });
92
+ if (memories.length === 0) {
93
+ const filters = [status !== "active" ? `status=${status}` : "", kind ? `kind=${kind}` : "", topic ? `topic=${topic}` : ""].filter(Boolean).join(", ");
94
+ return toolText(`No memories matched${filters ? ` (${filters})` : ""}.`);
95
+ }
96
+ const lines = [
97
+ `Found ${memories.length} ${status === "all" ? "" : `${status} `}memor${memories.length === 1 ? "y" : "ies"}:`,
98
+ ...memories.map((memory) => `- ${renderMemoryLine(memory)}`),
99
+ ];
100
+ return toolText(lines.join("\n"));
101
+ },
102
+ });
103
+
104
+ this.api.registerTool({
105
+ name: "memory_labels",
106
+ description: "List existing ClawMem schema labels so the agent can reuse current kinds and topics before adding new ones.",
107
+ required: true,
108
+ parameters: {
109
+ type: "object",
110
+ additionalProperties: false,
111
+ properties: {
112
+ agentId: { type: "string", minLength: 1, description: "Optional agent route override. Defaults to the current agent when available." },
113
+ limitTopics: { type: "integer", minimum: 1, maximum: 200, description: "Maximum number of topic labels to display." },
114
+ },
115
+ },
116
+ execute: async (_id: string, params: unknown) => {
117
+ const p = asRecord(params);
118
+ const agentId = this.resolveToolAgentId(p.agentId);
119
+ if (!(await this.ensureConfigured(agentId))) return toolText(`ClawMem route for agent "${agentId}" is not configured.`);
120
+ const { mem } = this.getServices(agentId);
121
+ const schema = await mem.listSchema();
122
+ const rawLimit = typeof p.limitTopics === "number" && Number.isFinite(p.limitTopics) ? Math.floor(p.limitTopics) : 50;
123
+ const limitTopics = Math.min(200, Math.max(1, rawLimit));
124
+ const kinds = schema.kinds.length > 0 ? schema.kinds.map((kind) => `- kind:${kind}`).join("\n") : "- None";
125
+ const topics = schema.topics.length > 0 ? schema.topics.slice(0, limitTopics).map((topic) => `- topic:${topic}`).join("\n") : "- None";
126
+ const extra = schema.topics.length > limitTopics ? `\n- ...and ${schema.topics.length - limitTopics} more topics` : "";
127
+ return toolText([
128
+ "Current ClawMem schema labels:",
129
+ "",
130
+ "Kinds:",
131
+ kinds,
132
+ "",
133
+ "Topics:",
134
+ `${topics}${extra}`,
135
+ ].join("\n"));
136
+ },
137
+ });
138
+
139
+ this.api.registerTool({
140
+ name: "memory_recall",
141
+ description: "Search ClawMem active memories for relevant prior facts, decisions, conventions, and lessons.",
142
+ required: true,
143
+ parameters: {
144
+ type: "object",
145
+ additionalProperties: false,
146
+ properties: {
147
+ query: { type: "string", minLength: 1, description: "What to recall from memory." },
148
+ limit: { type: "integer", minimum: 1, maximum: 20, description: "Maximum number of memories to return." },
149
+ agentId: { type: "string", minLength: 1, description: "Optional agent route override. Defaults to the current agent when available." },
150
+ },
151
+ required: ["query"],
152
+ },
153
+ execute: async (_id: string, params: unknown) => {
154
+ const p = asRecord(params);
155
+ const query = typeof p.query === "string" ? p.query.trim() : "";
156
+ if (!query) return toolText("Query is empty.");
157
+ const agentId = this.resolveToolAgentId(p.agentId);
158
+ if (!(await this.ensureConfigured(agentId))) return toolText(`ClawMem route for agent "${agentId}" is not configured.`);
159
+ const { mem } = this.getServices(agentId);
160
+ const rawLimit = typeof p.limit === "number" && Number.isFinite(p.limit) ? Math.floor(p.limit) : this.config.memoryRecallLimit;
161
+ const limit = Math.min(20, Math.max(1, rawLimit));
162
+ const memories = await mem.search(query, limit);
163
+ if (memories.length === 0) return toolText(`No active memories matched "${query}".`);
164
+ const text = [
165
+ `Found ${memories.length} active memor${memories.length === 1 ? "y" : "ies"} for "${query}":`,
166
+ ...memories.map((memory) => `- ${renderMemoryLine(memory)}`),
167
+ ].join("\n");
168
+ return toolText(text);
169
+ },
170
+ });
171
+
172
+ this.api.registerTool({
173
+ name: "memory_get",
174
+ description: "Fetch one ClawMem memory by memory id or issue number so the agent can verify an exact record.",
175
+ required: true,
176
+ parameters: {
177
+ type: "object",
178
+ additionalProperties: false,
179
+ properties: {
180
+ memoryId: { type: "string", minLength: 1, description: "The memory id or issue number to retrieve." },
181
+ status: { type: "string", enum: ["active", "stale", "all"], description: "Which status bucket to search. Defaults to all." },
182
+ agentId: { type: "string", minLength: 1, description: "Optional agent route override. Defaults to the current agent when available." },
183
+ },
184
+ required: ["memoryId"],
185
+ },
186
+ execute: async (_id: string, params: unknown) => {
187
+ const p = asRecord(params);
188
+ const memoryId = typeof p.memoryId === "string" ? p.memoryId.trim() : "";
189
+ if (!memoryId) return toolText("memoryId is empty.");
190
+ const agentId = this.resolveToolAgentId(p.agentId);
191
+ if (!(await this.ensureConfigured(agentId))) return toolText(`ClawMem route for agent "${agentId}" is not configured.`);
192
+ const { mem } = this.getServices(agentId);
193
+ const status = p.status === "active" || p.status === "stale" ? p.status : "all";
194
+ const memory = await mem.get(memoryId, status);
195
+ if (!memory) return toolText(`No ${status === "all" ? "" : `${status} `}memory matched id "${memoryId}".`);
196
+ return toolText(renderMemoryBlock(memory));
197
+ },
198
+ });
199
+
200
+ this.api.registerTool({
201
+ name: "memory_store",
202
+ description: "Store a durable ClawMem memory immediately instead of waiting for session finalization.",
203
+ required: true,
204
+ parameters: {
205
+ type: "object",
206
+ additionalProperties: false,
207
+ properties: {
208
+ detail: { type: "string", minLength: 1, description: "The durable fact, lesson, decision, or preference to remember." },
209
+ kind: { type: "string", minLength: 1, description: "Optional schema kind, for example lesson, convention, skill, or task." },
210
+ topics: {
211
+ type: "array",
212
+ description: "Optional topic labels to improve future retrieval.",
213
+ items: { type: "string", minLength: 1 },
214
+ minItems: 1,
215
+ maxItems: 10,
216
+ },
217
+ sessionId: { type: "string", minLength: 1, description: "Optional source session id label. Defaults to manual." },
218
+ agentId: { type: "string", minLength: 1, description: "Optional agent route override. Defaults to the current agent when available." },
219
+ },
220
+ required: ["detail"],
221
+ },
222
+ execute: async (_id: string, params: unknown) => {
223
+ const p = asRecord(params);
224
+ const detail = typeof p.detail === "string" ? p.detail.trim() : "";
225
+ if (!detail) return toolText("Detail is empty.");
226
+ const agentId = this.resolveToolAgentId(p.agentId);
227
+ if (!(await this.ensureConfigured(agentId))) return toolText(`ClawMem route for agent "${agentId}" is not configured.`);
228
+ const { mem } = this.getServices(agentId);
229
+ const sessionId = typeof p.sessionId === "string" && p.sessionId.trim() ? p.sessionId.trim() : "manual";
230
+ const kind = typeof p.kind === "string" && p.kind.trim() ? p.kind.trim() : undefined;
231
+ const topics = Array.isArray(p.topics) ? p.topics.filter((topic): topic is string => typeof topic === "string" && topic.trim().length > 0) : undefined;
232
+ const result = await mem.store({ detail, ...(kind ? { kind } : {}), ...(topics && topics.length > 0 ? { topics } : {}) }, sessionId);
233
+ if (!result.created) return toolText(`Memory already exists.\n${renderMemoryBlock(result.memory)}`);
234
+ return toolText(`Stored memory.\n${renderMemoryBlock(result.memory)}`);
235
+ },
236
+ });
237
+
238
+ this.api.registerTool({
239
+ name: "memory_update",
240
+ description: "Update an existing ClawMem memory in place when the same canonical fact or task has evolved.",
241
+ required: true,
242
+ parameters: {
243
+ type: "object",
244
+ additionalProperties: false,
245
+ properties: {
246
+ memoryId: { type: "string", minLength: 1, description: "The memory id or issue number to update." },
247
+ detail: { type: "string", minLength: 1, description: "Optional replacement detail text for the same memory record." },
248
+ kind: { type: "string", minLength: 1, description: "Optional replacement kind label." },
249
+ topics: {
250
+ type: "array",
251
+ description: "Optional replacement topic labels.",
252
+ items: { type: "string", minLength: 1 },
253
+ minItems: 1,
254
+ maxItems: 10,
255
+ },
256
+ agentId: { type: "string", minLength: 1, description: "Optional agent route override. Defaults to the current agent when available." },
257
+ },
258
+ required: ["memoryId"],
259
+ },
260
+ execute: async (_id: string, params: unknown) => {
261
+ const p = asRecord(params);
262
+ const memoryId = typeof p.memoryId === "string" ? p.memoryId.trim() : "";
263
+ if (!memoryId) return toolText("memoryId is empty.");
264
+ const detail = typeof p.detail === "string" && p.detail.trim() ? p.detail.trim() : undefined;
265
+ const kind = typeof p.kind === "string" && p.kind.trim() ? p.kind.trim() : undefined;
266
+ const topics = Array.isArray(p.topics) ? p.topics.filter((topic): topic is string => typeof topic === "string" && topic.trim().length > 0) : undefined;
267
+ if (!detail && kind === undefined && topics === undefined) return toolText("Provide at least one of detail, kind, or topics.");
268
+ const agentId = this.resolveToolAgentId(p.agentId);
269
+ if (!(await this.ensureConfigured(agentId))) return toolText(`ClawMem route for agent "${agentId}" is not configured.`);
270
+ const { mem } = this.getServices(agentId);
271
+ let updated;
272
+ try {
273
+ updated = await mem.update(memoryId, { ...(detail ? { detail } : {}), ...(kind !== undefined ? { kind } : {}), ...(topics !== undefined ? { topics } : {}) });
274
+ } catch (error) {
275
+ return toolText(`Unable to update memory "${memoryId}": ${String(error)}`);
276
+ }
277
+ if (!updated) return toolText(`No memory matched id "${memoryId}".`);
278
+ return toolText(`Updated memory.\n${renderMemoryBlock(updated)}`);
279
+ },
280
+ });
281
+
282
+ this.api.registerTool({
283
+ name: "memory_forget",
284
+ description: "Mark an active ClawMem memory as stale when it is superseded or no longer true.",
285
+ required: true,
286
+ parameters: {
287
+ type: "object",
288
+ additionalProperties: false,
289
+ properties: {
290
+ memoryId: { type: "string", minLength: 1, description: "The memory id or issue number to mark stale." },
291
+ agentId: { type: "string", minLength: 1, description: "Optional agent route override. Defaults to the current agent when available." },
292
+ },
293
+ required: ["memoryId"],
294
+ },
295
+ execute: async (_id: string, params: unknown) => {
296
+ const p = asRecord(params);
297
+ const memoryId = typeof p.memoryId === "string" ? p.memoryId.trim() : "";
298
+ if (!memoryId) return toolText("memoryId is empty.");
299
+ const agentId = this.resolveToolAgentId(p.agentId);
300
+ if (!(await this.ensureConfigured(agentId))) return toolText(`ClawMem route for agent "${agentId}" is not configured.`);
301
+ const { mem } = this.getServices(agentId);
302
+ const forgotten = await mem.forget(memoryId);
303
+ if (!forgotten) return toolText(`No active memory matched id "${memoryId}".`);
304
+ return toolText(`Marked memory [${forgotten.memoryId}] stale: ${forgotten.detail}`);
305
+ },
306
+ });
307
+ }
308
+
309
+ private async handleBeforeAgentStart(prompt: unknown, agentId?: string): Promise<{ prependContext: string } | void> {
66
310
  const routeAgentId = normalizeAgentId(agentId);
67
311
  if (!(await this.ensureConfigured(routeAgentId))) return;
312
+ await this.runRequestMaintenance(routeAgentId);
313
+ if (typeof prompt !== "string" || prompt.trim().length < 5) return;
68
314
  try {
69
315
  const { mem } = this.getServices(routeAgentId);
70
316
  const memories = await mem.search(prompt, this.config.memoryRecallLimit);
@@ -138,7 +384,7 @@ class ClawMemService {
138
384
  if (!p.sessionId) return;
139
385
  const agentId = normalizeAgentId(p.agentId);
140
386
  if (!(await this.ensureConfigured(agentId))) return;
141
- const { conv, mem } = this.getServices(agentId);
387
+ const { conv } = this.getServices(agentId);
142
388
  const s = this.getOrCreate(p.sessionId, agentId);
143
389
  if (s.finalizedAt) return;
144
390
  s.sessionKey = p.sessionKey ?? s.sessionKey; s.sessionFile = p.sessionFile ?? s.sessionFile;
@@ -150,11 +396,9 @@ class ClawMemService {
150
396
  const next = snap.messages.slice(s.lastMirroredCount);
151
397
  let allOk = true;
152
398
  if (next.length > 0) { const n = await conv.appendComments(s.issueNumber!, next); s.lastMirroredCount += n; s.turnCount += n; allOk = n === next.length; }
153
- let summary = "pending";
154
- try { summary = await conv.generateSummary(s, snap); } catch (e) { summary = `failed: ${String(e)}`; }
155
399
  await conv.syncLabels(s, snap, true);
156
- await conv.syncBody(s, snap, summary, true);
157
- await mem.syncFromConversation(s, snap);
400
+ await conv.syncBody(s, snap, "pending", true);
401
+ s.summaryStatus = "pending";
158
402
  if (allOk) s.finalizedAt = new Date().toISOString();
159
403
  await this.persistState();
160
404
  }
@@ -226,13 +470,35 @@ class ClawMemService {
226
470
  if (!route.baseUrl) { this.api.logger.warn(`clawmem: cannot provision Git credentials for ${agentId} without a baseUrl`); return false; }
227
471
  try {
228
472
  const client = new GitHubIssueClient(route, this.api.logger);
229
- const sess = await client.createAnonymousSession();
473
+ const locale = Intl?.DateTimeFormat?.()?.resolvedOptions?.()?.locale ?? "";
474
+ const sess = await client.createAnonymousSession(locale);
230
475
  await this.persistAgentConfig(agentId, { baseUrl: route.baseUrl, authScheme: "token", token: sess.token, repo: sess.repo_full_name });
231
476
  this.config.agents[agentId] = { ...(this.config.agents[agentId] ?? {}), baseUrl: route.baseUrl, authScheme: "token", token: sess.token, repo: sess.repo_full_name };
232
477
  this.api.logger.info?.(`clawmem: provisioned Git credentials for agent ${agentId} -> ${sess.repo_full_name} via ${route.baseUrl}`);
233
478
  return true;
234
479
  } catch (error) { this.api.logger.warn(`clawmem: failed to provision Git credentials for agent ${agentId} via ${route.baseUrl}: ${String(error)}`); return false; }
235
480
  }
481
+ private warnIfInactiveMemorySlot(): void {
482
+ try {
483
+ const root = this.api.runtime.config.loadConfig();
484
+ const plugins = asRecord(root.plugins);
485
+ const slots = asRecord(plugins.slots);
486
+ const slot = typeof slots.memory === "string" ? String(slots.memory).trim() : "";
487
+ if (!slot) {
488
+ this.api.logger.warn(
489
+ `clawmem: plugins.slots.memory is not set, so OpenClaw may keep the default memory plugin active. Set plugins.slots.memory to "${this.api.id}" and restart the gateway.`,
490
+ );
491
+ return;
492
+ }
493
+ if (slot !== this.api.id) {
494
+ this.api.logger.warn(
495
+ `clawmem: plugins.slots.memory is "${slot}", so ClawMem is not the selected memory plugin. Set plugins.slots.memory to "${this.api.id}" and restart the gateway.`,
496
+ );
497
+ }
498
+ } catch (error) {
499
+ this.api.logger.warn(`clawmem: memory slot check failed: ${String(error)}`);
500
+ }
501
+ }
236
502
  private async persistAgentConfig(agentId: string, values: { baseUrl: string; authScheme: "token" | "bearer"; token: string; repo: string }): Promise<void> {
237
503
  const root = this.api.runtime.config.loadConfig();
238
504
  const plugins = root.plugins;
@@ -260,6 +526,57 @@ class ClawMemService {
260
526
  },
261
527
  });
262
528
  }
529
+ private async runRequestMaintenance(agentId: string): Promise<void> {
530
+ const sessions = Object.values(this.state.sessions)
531
+ .filter((session) => normalizeAgentId(session.agentId) === agentId)
532
+ .sort((a, b) => Date.parse(b.updatedAt ?? b.createdAt ?? "") - Date.parse(a.updatedAt ?? a.createdAt ?? ""))
533
+ .slice(0, 8);
534
+ if (sessions.length === 0) return;
535
+ const { conv, mem } = this.getServices(agentId);
536
+ let changed = false;
537
+ let workDone = 0;
538
+ for (const session of sessions) {
539
+ if (workDone >= 3) break;
540
+ const snap = await conv.loadSnapshot(session, []);
541
+ if (!conv.shouldMirror(session.sessionId, snap.messages) || snap.messages.length === 0) continue;
542
+ if (!session.issueNumber) {
543
+ await conv.ensureIssue(session, snap);
544
+ changed = true;
545
+ }
546
+ if (session.summaryStatus === "pending") {
547
+ try {
548
+ const result = await conv.generateSummaryAndTitle(session, snap);
549
+ await conv.syncLabels(session, snap, true);
550
+ await conv.syncBody(session, snap, result.summary, true, result.title);
551
+ session.summaryStatus = "complete";
552
+ if (result.title?.trim()) {
553
+ session.issueTitle = result.title.trim();
554
+ session.titleSource = "llm";
555
+ }
556
+ this.maybeAutoNameRepo(agentId, result.summary, result.title);
557
+ changed = true;
558
+ workDone++;
559
+ } catch (error) {
560
+ this.warn(`request-scoped summary sync for ${session.sessionId}`, error);
561
+ }
562
+ }
563
+ if (session.titleSource !== "llm" && snap.messages.length >= 2) {
564
+ await conv.syncTitle(session, snap);
565
+ changed = true;
566
+ workDone++;
567
+ }
568
+ if (snap.messages.length >= 2 && snap.messages.length > (session.lastMemorySyncCount ?? 0)) {
569
+ const ok = await mem.syncFromConversation(session, snap);
570
+ if (ok) {
571
+ session.lastMemorySyncCount = snap.messages.length;
572
+ changed = true;
573
+ }
574
+ workDone++;
575
+ }
576
+ }
577
+ if (changed) await this.persistState();
578
+ }
579
+
263
580
  private getServices(agentId?: string): { conv: ConversationMirror; mem: MemoryStore } {
264
581
  const client = new GitHubIssueClient(resolveAgentRoute(this.config, agentId), this.api.logger);
265
582
  return {
@@ -267,9 +584,55 @@ class ClawMemService {
267
584
  mem: new MemoryStore(client, this.api, this.config),
268
585
  };
269
586
  }
587
+ private resolveToolAgentId(agentId: unknown): string {
588
+ return normalizeAgentId(typeof agentId === "string" && agentId.trim() ? agentId : process.env.OPENCLAW_AGENT_ID);
589
+ }
590
+ /**
591
+ * After finalization, check if the repo still has an empty/default description.
592
+ * If so, use the conversation summary to suggest a meaningful name and update
593
+ * the repo description automatically. Best-effort, fire-and-forget.
594
+ */
595
+ private maybeAutoNameRepo(agentId: string, summary: string, title?: string): void {
596
+ if (!summary || summary.startsWith("failed:") || summary === "pending") return;
597
+ const snippet = title || summary.slice(0, 100);
598
+ void (async () => {
599
+ try {
600
+ const client = new GitHubIssueClient(resolveAgentRoute(this.config, agentId), this.api.logger);
601
+ const repo = await client.getRepoInfo();
602
+ // Only auto-name if description is still empty or a default placeholder.
603
+ if (repo.description && repo.description !== "My Memory Space" && repo.description !== "我的记忆空间" && repo.description !== "マイメモリースペース") return;
604
+ // Use the conversation title or summary as a lightweight description.
605
+ await client.updateRepoDescription(snippet);
606
+ this.api.logger.info?.(`clawmem: auto-named repo to "${snippet}"`);
607
+ } catch (e) {
608
+ this.api.logger.warn(`clawmem: auto-name repo failed: ${String(e)}`);
609
+ }
610
+ })();
611
+ }
270
612
  private warn(scope: string, error: unknown): void { this.api.logger.warn(`clawmem: ${scope} failed: ${String(error)}`); }
271
613
  }
272
614
 
273
615
  function asRecord(v: unknown): Record<string, unknown> { return v && typeof v === "object" ? (v as Record<string, unknown>) : {}; }
616
+ function toolText(text: string): { content: Array<{ type: "text"; text: string }> } {
617
+ return { content: [{ type: "text", text }] };
618
+ }
619
+ function renderMemoryLine(memory: { memoryId: string; title?: string; detail: string; kind?: string; topics?: string[]; status: "active" | "stale" }): string {
620
+ const schema = [memory.kind ? `kind:${memory.kind}` : "", ...(memory.topics ?? []).map((topic) => `topic:${topic}`)].filter(Boolean).join(", ");
621
+ return `[${memory.memoryId}] ${memory.title || "Memory"}${schema ? ` (${schema})` : ""}${memory.status === "stale" ? " [stale]" : ""}: ${memory.detail}`;
622
+ }
623
+ function renderMemoryBlock(memory: { memoryId: string; issueNumber?: number; title?: string; detail: string; kind?: string; topics?: string[]; status: "active" | "stale"; sessionId?: string; date?: string }): string {
624
+ const lines = [
625
+ `Memory ID: ${memory.memoryId}`,
626
+ ...(typeof memory.issueNumber === "number" ? [`Issue Number: ${memory.issueNumber}`] : []),
627
+ `Status: ${memory.status}`,
628
+ `Title: ${memory.title || "Memory"}`,
629
+ ...(memory.kind ? [`Kind: ${memory.kind}`] : []),
630
+ ...(memory.topics && memory.topics.length > 0 ? [`Topics: ${memory.topics.join(", ")}`] : []),
631
+ ...(memory.sessionId ? [`Session: ${memory.sessionId}`] : []),
632
+ ...(memory.date ? [`Date: ${memory.date}`] : []),
633
+ `Detail: ${memory.detail}`,
634
+ ];
635
+ return lines.join("\n");
636
+ }
274
637
 
275
638
  export function createClawMemPlugin(api: OpenClawPluginApi): void { new ClawMemService(api).register(); }
package/src/state.ts CHANGED
@@ -42,9 +42,17 @@ function sanitizeState(value: unknown): PluginState {
42
42
  raw.sessions && typeof raw.sessions === "object"
43
43
  ? (raw.sessions as Record<string, unknown>)
44
44
  : {};
45
+ const migrations: Record<string, string> = {};
46
+ if (raw.migrations && typeof raw.migrations === "object") {
47
+ for (const [k, v] of Object.entries(raw.migrations as Record<string, unknown>)) {
48
+ const s = readString(v);
49
+ if (s) migrations[k] = s;
50
+ }
51
+ }
45
52
  const out: PluginState = {
46
53
  version: 2,
47
54
  sessions: {},
55
+ ...(Object.keys(migrations).length > 0 ? { migrations } : {}),
48
56
  };
49
57
  for (const [storedKey, sessionValue] of Object.entries(sessions)) {
50
58
  if (!sessionValue || typeof sessionValue !== "object" || !storedKey.trim()) {
@@ -63,8 +71,11 @@ function sanitizeState(value: unknown): PluginState {
63
71
  agentId,
64
72
  issueNumber: readNumber(rawSession.issueNumber),
65
73
  issueTitle: readString(rawSession.issueTitle),
74
+ titleSource: readEnum(rawSession.titleSource, ["placeholder", "llm"]),
66
75
  lastMirroredCount: readNumber(rawSession.lastMirroredCount) ?? 0,
67
76
  turnCount: readNumber(rawSession.turnCount) ?? 0,
77
+ lastMemorySyncCount: readNumber(rawSession.lastMemorySyncCount),
78
+ summaryStatus: readEnum(rawSession.summaryStatus, ["pending", "complete"]),
68
79
  finalizedAt: readString(rawSession.finalizedAt),
69
80
  lastSummaryHash: readString(rawSession.lastSummaryHash),
70
81
  lastTurnHash: readString(rawSession.lastTurnHash),
@@ -83,6 +94,11 @@ function readString(value: unknown): string | undefined {
83
94
  return trimmed ? trimmed : undefined;
84
95
  }
85
96
 
97
+ function readEnum<T extends string>(value: unknown, allowed: T[]): T | undefined {
98
+ const s = readString(value);
99
+ return s && (allowed as string[]).includes(s) ? (s as T) : undefined;
100
+ }
101
+
86
102
  function readNumber(value: unknown): number | undefined {
87
103
  if (typeof value !== "number" || !Number.isFinite(value)) {
88
104
  return undefined;
package/src/types.ts CHANGED
@@ -26,16 +26,26 @@ export type ClawMemResolvedRoute = {
26
26
  export type AnonymousSessionResponse = { token: string; owner_login: string; repo_name: string; repo_full_name: string };
27
27
  export type SessionMirrorState = {
28
28
  sessionId: string; sessionKey?: string; sessionFile?: string; agentId?: string;
29
- issueNumber?: number; issueTitle?: string;
29
+ issueNumber?: number; issueTitle?: string; titleSource?: "placeholder" | "llm";
30
30
  lastMirroredCount: number; turnCount: number; lastAssistantText?: string;
31
+ lastMemorySyncCount?: number;
32
+ summaryStatus?: "pending" | "complete";
31
33
  finalizedAt?: string; lastSummaryHash?: string; lastTurnHash?: string;
32
34
  createdAt?: string; updatedAt?: string;
33
35
  };
34
- export type PluginState = { version: 2; sessions: Record<string, SessionMirrorState> };
36
+ export type PluginState = { version: 2; sessions: Record<string, SessionMirrorState>; migrations?: Record<string, string> };
35
37
  export type NormalizedMessage = { role: string; text: string; toolName?: string; timestamp?: string; stopReason?: string };
36
38
  export type TranscriptSnapshot = { sessionId?: string; messages: NormalizedMessage[] };
39
+ export type MemoryDraft = { detail: string; kind?: string; topics?: string[] };
40
+ export type MemorySchema = { kinds: string[]; topics: string[] };
41
+ export type MemoryListOptions = {
42
+ status?: "active" | "stale" | "all";
43
+ kind?: string;
44
+ topic?: string;
45
+ limit?: number;
46
+ };
37
47
  export type ParsedMemoryIssue = {
38
48
  issueNumber: number; title: string; memoryId: string; memoryHash?: string;
39
49
  sessionId: string; date: string; detail: string;
40
- topics?: string[]; status: "active" | "stale";
50
+ kind?: string; topics?: string[]; status: "active" | "stale";
41
51
  };