@memoryrelay/plugin-memoryrelay-ai 0.15.7 → 0.16.1

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.
Files changed (42) hide show
  1. package/index.ts +493 -4868
  2. package/openclaw.plugin.json +41 -3
  3. package/package.json +1 -1
  4. package/src/client/memoryrelay-client.ts +816 -0
  5. package/src/context/namespace-router.ts +19 -0
  6. package/src/context/request-context.ts +39 -0
  7. package/src/context/session-resolver.ts +93 -0
  8. package/src/filters/content-patterns.ts +32 -0
  9. package/src/filters/noise-patterns.ts +33 -0
  10. package/src/filters/non-interactive.ts +30 -0
  11. package/src/hooks/activity.ts +51 -0
  12. package/src/hooks/agent-end.ts +48 -0
  13. package/src/hooks/before-agent-start.ts +109 -0
  14. package/src/hooks/before-prompt-build.ts +46 -0
  15. package/src/hooks/compaction.ts +51 -0
  16. package/src/hooks/privacy.ts +44 -0
  17. package/src/hooks/session-lifecycle.ts +47 -0
  18. package/src/hooks/subagent.ts +62 -0
  19. package/src/pipelines/capture/content-strip.ts +14 -0
  20. package/src/pipelines/capture/dedup.ts +17 -0
  21. package/src/pipelines/capture/index.ts +13 -0
  22. package/src/pipelines/capture/message-filter.ts +16 -0
  23. package/src/pipelines/capture/store.ts +33 -0
  24. package/src/pipelines/capture/trigger-gate.ts +21 -0
  25. package/src/pipelines/capture/truncate.ts +16 -0
  26. package/src/pipelines/recall/format.ts +30 -0
  27. package/src/pipelines/recall/index.ts +12 -0
  28. package/src/pipelines/recall/rank.ts +40 -0
  29. package/src/pipelines/recall/scope-resolver.ts +20 -0
  30. package/src/pipelines/recall/search.ts +43 -0
  31. package/src/pipelines/recall/trigger-gate.ts +17 -0
  32. package/src/pipelines/runner.ts +25 -0
  33. package/src/pipelines/types.ts +157 -0
  34. package/src/tools/agent-tools.ts +127 -0
  35. package/src/tools/decision-tools.ts +309 -0
  36. package/src/tools/entity-tools.ts +215 -0
  37. package/src/tools/health-tools.ts +42 -0
  38. package/src/tools/memory-tools.ts +690 -0
  39. package/src/tools/pattern-tools.ts +250 -0
  40. package/src/tools/project-tools.ts +444 -0
  41. package/src/tools/session-tools.ts +195 -0
  42. package/src/tools/v2-tools.ts +228 -0
@@ -0,0 +1,690 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ import type { PluginConfig } from "../pipelines/types.js";
3
+ import type { MemoryRelayClient } from "../client/memoryrelay-client.js";
4
+ import type { SessionResolver } from "../context/session-resolver.js";
5
+
6
+ export function registerMemoryTools(
7
+ api: OpenClawPluginApi,
8
+ config: PluginConfig,
9
+ client: MemoryRelayClient,
10
+ sessionResolver: SessionResolver,
11
+ isToolEnabled: (name: string) => boolean,
12
+ ): void {
13
+ const defaultProject = config.defaultProject;
14
+
15
+ // --------------------------------------------------------------------------
16
+ // 1. memory_store
17
+ // --------------------------------------------------------------------------
18
+ if (isToolEnabled("memory_store")) {
19
+ api.registerTool((ctx) => ({
20
+
21
+ name: "memory_store",
22
+ description:
23
+ "Store a new memory in MemoryRelay. Use this to save important information, facts, preferences, or context that should be remembered for future conversations." +
24
+ (defaultProject ? ` Project defaults to '${defaultProject}' if not specified.` : "") +
25
+ " Set deduplicate=true to avoid storing near-duplicate memories.",
26
+ parameters: {
27
+ type: "object",
28
+ properties: {
29
+ content: {
30
+ type: "string",
31
+ description: "The memory content to store. Be specific and include relevant context.",
32
+ },
33
+ metadata: {
34
+ type: "object",
35
+ description: "Optional key-value metadata to attach to the memory",
36
+ additionalProperties: { type: "string" },
37
+ },
38
+ deduplicate: {
39
+ type: "boolean",
40
+ description: "If true, check for duplicate memories before storing. Default false.",
41
+ },
42
+ dedup_threshold: {
43
+ type: "number",
44
+ description: "Similarity threshold for deduplication (0-1). Default 0.95.",
45
+ },
46
+ project: {
47
+ type: "string",
48
+ description: "Project slug to associate with this memory.",
49
+ },
50
+ importance: {
51
+ type: "number",
52
+ description: "Importance score (0-1). Higher values are retained longer.",
53
+ },
54
+ tier: {
55
+ type: "string",
56
+ description: "Memory tier: hot, warm, or cold.",
57
+ enum: ["hot", "warm", "cold"],
58
+ },
59
+ session_id: {
60
+ type: "string",
61
+ description: "Optional MemoryRelay session UUID to associate this memory with. If omitted and project is set, plugin auto-creates session via external_id.",
62
+ },
63
+ scope: {
64
+ type: "string",
65
+ description: "Memory scope: 'session' (current conversation) or 'long-term' (persistent). Default: 'long-term'.",
66
+ enum: ["session", "long-term"],
67
+ },
68
+ },
69
+ required: ["content"],
70
+ },
71
+ execute: async (
72
+ _id,
73
+ args: {
74
+ content: string;
75
+ metadata?: Record<string, string>;
76
+ deduplicate?: boolean;
77
+ dedup_threshold?: number;
78
+ project?: string;
79
+ importance?: number;
80
+ tier?: string;
81
+ session_id?: string;
82
+ scope?: string;
83
+ },
84
+ ) => {
85
+ try {
86
+ const { content, metadata: rawMetadata, session_id: explicitSessionId, scope, ...opts } = args;
87
+
88
+ // Auto-tag with sender identity from tool context
89
+ const metadata = rawMetadata || {};
90
+ if (ctx.requesterSenderId && !metadata.sender_id) {
91
+ metadata.sender_id = ctx.requesterSenderId;
92
+ }
93
+
94
+ // Apply defaultProject fallback before session resolution
95
+ if (!opts.project && defaultProject) opts.project = defaultProject;
96
+
97
+ // Get session_id from SessionResolver if project context available
98
+ // Priority: explicit session_id > context session > no session
99
+ let sessionId: string | undefined = explicitSessionId;
100
+
101
+ if (!sessionId && (opts.project || ctx.workspaceDir)) {
102
+ try {
103
+ const entry = await sessionResolver.resolve({
104
+ sessionKey: opts.project || `workspace-${(ctx.workspaceDir || "").split(/[/\\]/).pop()}`,
105
+ agentId: null,
106
+ channel: null,
107
+ trigger: null,
108
+ prompt: "",
109
+ isSubagent: false,
110
+ parentSessionKey: null,
111
+ namespace: opts.project || "default",
112
+ timestamp: Date.now(),
113
+ });
114
+ sessionId = entry.sessionId;
115
+ } catch {
116
+ // Session resolution failed — continue without session
117
+ }
118
+ }
119
+
120
+ // Build request options with session_id as top-level parameter
121
+ const storeOpts = {
122
+ ...opts,
123
+ ...(sessionId && { session_id: sessionId }),
124
+ ...(scope && { scope }),
125
+ };
126
+
127
+ const memory = await client.store(content, metadata, storeOpts);
128
+ return {
129
+ content: [
130
+ {
131
+ type: "text",
132
+ text: `Memory stored successfully (id: ${memory.id.slice(0, 8)}...)${sessionId ? ` in session ${sessionId.slice(0, 8)}...` : ''}`,
133
+ },
134
+ ],
135
+ details: { id: memory.id, stored: true, session_id: sessionId },
136
+ };
137
+ } catch (err) {
138
+ return {
139
+ content: [{ type: "text", text: `Failed to store memory: ${String(err)}` }],
140
+ details: { error: String(err) },
141
+ };
142
+ }
143
+ },
144
+ }),
145
+ { name: "memory_store" },
146
+ );
147
+ }
148
+
149
+ // --------------------------------------------------------------------------
150
+ // 2. memory_recall
151
+ // --------------------------------------------------------------------------
152
+ if (isToolEnabled("memory_recall")) {
153
+ api.registerTool((ctx) => ({
154
+
155
+ name: "memory_recall",
156
+ description:
157
+ "Search memories using natural language. Returns the most relevant memories based on semantic similarity to the query." +
158
+ (defaultProject ? ` Results scoped to project '${defaultProject}' by default; pass project explicitly to override or omit to search all.` : ""),
159
+ parameters: {
160
+ type: "object",
161
+ properties: {
162
+ query: {
163
+ type: "string",
164
+ description: "Natural language search query",
165
+ },
166
+ limit: {
167
+ type: "number",
168
+ description: "Maximum results (1-50). Default 5.",
169
+ minimum: 1,
170
+ maximum: 50,
171
+ },
172
+ threshold: {
173
+ type: "number",
174
+ description: "Minimum similarity threshold (0-1). Default 0.3.",
175
+ },
176
+ project: {
177
+ type: "string",
178
+ description: "Filter by project slug.",
179
+ },
180
+ tier: {
181
+ type: "string",
182
+ description: "Filter by memory tier: hot, warm, or cold.",
183
+ enum: ["hot", "warm", "cold"],
184
+ },
185
+ min_importance: {
186
+ type: "number",
187
+ description: "Minimum importance score filter (0-1).",
188
+ },
189
+ compress: {
190
+ type: "boolean",
191
+ description: "If true, compress results for token efficiency.",
192
+ },
193
+ scope: {
194
+ type: "string",
195
+ description: "Search scope: 'session', 'long-term', or 'all'. Default: 'all'.",
196
+ enum: ["session", "long-term", "all"],
197
+ },
198
+ },
199
+ required: ["query"],
200
+ },
201
+ execute: async (
202
+ _id,
203
+ args: {
204
+ query: string;
205
+ limit?: number;
206
+ threshold?: number;
207
+ project?: string;
208
+ tier?: string;
209
+ min_importance?: number;
210
+ compress?: boolean;
211
+ scope?: string;
212
+ },
213
+ ) => {
214
+ try {
215
+ const {
216
+ query,
217
+ limit = 5,
218
+ threshold,
219
+ project,
220
+ tier,
221
+ min_importance,
222
+ compress,
223
+ scope,
224
+ } = args;
225
+ const searchThreshold = threshold ?? config?.recallThreshold ?? 0.3;
226
+ const searchProject = project ?? defaultProject;
227
+ const results = await client.search(query, limit, searchThreshold, {
228
+ project: searchProject,
229
+ tier,
230
+ min_importance,
231
+ compress,
232
+ ...(scope && { scope }),
233
+ });
234
+
235
+ if (results.length === 0) {
236
+ return {
237
+ content: [{ type: "text", text: "No relevant memories found." }],
238
+ details: { count: 0 },
239
+ };
240
+ }
241
+
242
+ const formatted = results
243
+ .map(
244
+ (r) =>
245
+ `- [${r.score.toFixed(2)}] ${r.memory.content.slice(0, 200)}${
246
+ r.memory.content.length > 200 ? "..." : ""
247
+ }`,
248
+ )
249
+ .join("\n");
250
+
251
+ return {
252
+ content: [
253
+ {
254
+ type: "text",
255
+ text: `Found ${results.length} relevant memories:\n${formatted}`,
256
+ },
257
+ ],
258
+ details: {
259
+ count: results.length,
260
+ memories: results.map((r) => ({
261
+ id: r.memory.id,
262
+ content: r.memory.content,
263
+ score: r.score,
264
+ })),
265
+ },
266
+ };
267
+ } catch (err) {
268
+ return {
269
+ content: [{ type: "text", text: `Search failed: ${String(err)}` }],
270
+ details: { error: String(err) },
271
+ };
272
+ }
273
+ },
274
+ }),
275
+ { name: "memory_recall" },
276
+ );
277
+ }
278
+
279
+ // --------------------------------------------------------------------------
280
+ // 3. memory_forget
281
+ // --------------------------------------------------------------------------
282
+ if (isToolEnabled("memory_forget")) {
283
+ api.registerTool((ctx) => ({
284
+
285
+ name: "memory_forget",
286
+ description: "Delete a memory by ID, or search by query to find candidates. Provide memoryId for direct deletion, or query to search first. A single high-confidence match (>0.9) is auto-deleted; otherwise candidates are listed for you to choose.",
287
+ parameters: {
288
+ type: "object",
289
+ properties: {
290
+ memoryId: {
291
+ type: "string",
292
+ description: "Memory ID to delete",
293
+ },
294
+ query: {
295
+ type: "string",
296
+ description: "Search query to find memory",
297
+ },
298
+ },
299
+ },
300
+ execute: async (_id, { memoryId, query }: { memoryId?: string; query?: string }) => {
301
+ if (memoryId) {
302
+ try {
303
+ await client.delete(memoryId);
304
+ return {
305
+ content: [{ type: "text", text: `Memory ${memoryId.slice(0, 8)}... deleted.` }],
306
+ details: { action: "deleted", id: memoryId },
307
+ };
308
+ } catch (err) {
309
+ return {
310
+ content: [{ type: "text", text: `Delete failed: ${String(err)}` }],
311
+ details: { error: String(err) },
312
+ };
313
+ }
314
+ }
315
+
316
+ if (query) {
317
+ const results = await client.search(query, 5, 0.5, { project: defaultProject });
318
+
319
+ if (results.length === 0) {
320
+ return {
321
+ content: [{ type: "text", text: "No matching memories found." }],
322
+ details: { count: 0 },
323
+ };
324
+ }
325
+
326
+ // If single high-confidence match, delete it
327
+ if (results.length === 1 && results[0].score > 0.9) {
328
+ await client.delete(results[0].memory.id);
329
+ return {
330
+ content: [
331
+ { type: "text", text: `Forgotten: "${results[0].memory.content.slice(0, 60)}..."` },
332
+ ],
333
+ details: { action: "deleted", id: results[0].memory.id },
334
+ };
335
+ }
336
+
337
+ const list = results
338
+ .map((r) => `- [${r.memory.id}] ${r.memory.content.slice(0, 60)}...`)
339
+ .join("\n");
340
+
341
+ return {
342
+ content: [
343
+ {
344
+ type: "text",
345
+ text: `Found ${results.length} candidates. Specify memoryId:\n${list}`,
346
+ },
347
+ ],
348
+ details: { action: "candidates", count: results.length },
349
+ };
350
+ }
351
+
352
+ return {
353
+ content: [{ type: "text", text: "Provide query or memoryId." }],
354
+ details: { error: "missing_param" },
355
+ };
356
+ },
357
+ }),
358
+ { name: "memory_forget" },
359
+ );
360
+ }
361
+
362
+ // --------------------------------------------------------------------------
363
+ // 4. memory_list
364
+ // --------------------------------------------------------------------------
365
+ if (isToolEnabled("memory_list")) {
366
+ api.registerTool((ctx) => ({
367
+
368
+ name: "memory_list",
369
+ description: "List recent memories chronologically for this agent. Use to review what has been stored or to find memory IDs for update/delete operations.",
370
+ parameters: {
371
+ type: "object",
372
+ properties: {
373
+ limit: {
374
+ type: "number",
375
+ description: "Number of memories to return (1-100). Default 20.",
376
+ minimum: 1,
377
+ maximum: 100,
378
+ },
379
+ offset: {
380
+ type: "number",
381
+ description: "Offset for pagination. Default 0.",
382
+ minimum: 0,
383
+ },
384
+ scope: {
385
+ type: "string",
386
+ description: "List scope: 'session', 'long-term', or 'all'. Default: 'all'.",
387
+ enum: ["session", "long-term", "all"],
388
+ },
389
+ },
390
+ },
391
+ execute: async (_id, args: { limit?: number; offset?: number; scope?: string }) => {
392
+ try {
393
+ const memories = await client.list(args.limit ?? 20, args.offset ?? 0, {
394
+ ...(args.scope && { scope: args.scope }),
395
+ });
396
+ if (memories.length === 0) {
397
+ return {
398
+ content: [{ type: "text", text: "No memories found." }],
399
+ details: { count: 0 },
400
+ };
401
+ }
402
+ const formatted = memories
403
+ .map((m) => `- [${m.id}] ${m.content.slice(0, 120)}`)
404
+ .join("\n");
405
+ return {
406
+ content: [{ type: "text", text: `${memories.length} memories:\n${formatted}` }],
407
+ details: { count: memories.length, memories },
408
+ };
409
+ } catch (err) {
410
+ return {
411
+ content: [{ type: "text", text: `Failed to list memories: ${String(err)}` }],
412
+ details: { error: String(err) },
413
+ };
414
+ }
415
+ },
416
+ }),
417
+ { name: "memory_list" },
418
+ );
419
+ }
420
+
421
+ // --------------------------------------------------------------------------
422
+ // 5. memory_get
423
+ // --------------------------------------------------------------------------
424
+ if (isToolEnabled("memory_get")) {
425
+ api.registerTool((ctx) => ({
426
+
427
+ name: "memory_get",
428
+ description: "Retrieve a specific memory by its ID.",
429
+ parameters: {
430
+ type: "object",
431
+ properties: {
432
+ id: {
433
+ type: "string",
434
+ description: "The memory ID (UUID) to retrieve.",
435
+ },
436
+ },
437
+ required: ["id"],
438
+ },
439
+ execute: async (_id, args: { id: string }) => {
440
+ try {
441
+ const memory = await client.get(args.id);
442
+ return {
443
+ content: [{ type: "text", text: JSON.stringify(memory, null, 2) }],
444
+ details: { memory },
445
+ };
446
+ } catch (err) {
447
+ return {
448
+ content: [{ type: "text", text: `Failed to get memory: ${String(err)}` }],
449
+ details: { error: String(err) },
450
+ };
451
+ }
452
+ },
453
+ }),
454
+ { name: "memory_get" },
455
+ );
456
+ }
457
+
458
+ // --------------------------------------------------------------------------
459
+ // 6. memory_update
460
+ // --------------------------------------------------------------------------
461
+ if (isToolEnabled("memory_update")) {
462
+ api.registerTool((ctx) => ({
463
+
464
+ name: "memory_update",
465
+ description: "Update the content of an existing memory. Use to correct or expand stored information.",
466
+ parameters: {
467
+ type: "object",
468
+ properties: {
469
+ id: {
470
+ type: "string",
471
+ description: "The memory ID (UUID) to update.",
472
+ },
473
+ content: {
474
+ type: "string",
475
+ description: "The new content to replace the existing memory.",
476
+ },
477
+ metadata: {
478
+ type: "object",
479
+ description: "Updated metadata (replaces existing).",
480
+ additionalProperties: { type: "string" },
481
+ },
482
+ },
483
+ required: ["id", "content"],
484
+ },
485
+ execute: async (_id, args: { id: string; content: string; metadata?: Record<string, string> }) => {
486
+ try {
487
+ const memory = await client.update(args.id, args.content, args.metadata);
488
+ return {
489
+ content: [{ type: "text", text: `Memory ${args.id.slice(0, 8)}... updated.` }],
490
+ details: { id: memory.id, updated: true },
491
+ };
492
+ } catch (err) {
493
+ return {
494
+ content: [{ type: "text", text: `Failed to update memory: ${String(err)}` }],
495
+ details: { error: String(err) },
496
+ };
497
+ }
498
+ },
499
+ }),
500
+ { name: "memory_update" },
501
+ );
502
+ }
503
+
504
+ // --------------------------------------------------------------------------
505
+ // 7. memory_batch_store
506
+ // --------------------------------------------------------------------------
507
+ if (isToolEnabled("memory_batch_store")) {
508
+ api.registerTool((ctx) => ({
509
+
510
+ name: "memory_batch_store",
511
+ description: "Store multiple memories at once. More efficient than individual calls for bulk storage.",
512
+ parameters: {
513
+ type: "object",
514
+ properties: {
515
+ memories: {
516
+ type: "array",
517
+ description: "Array of memories to store.",
518
+ items: {
519
+ type: "object",
520
+ properties: {
521
+ content: { type: "string", description: "Memory content." },
522
+ metadata: {
523
+ type: "object",
524
+ description: "Optional metadata.",
525
+ additionalProperties: { type: "string" },
526
+ },
527
+ },
528
+ required: ["content"],
529
+ },
530
+ },
531
+ },
532
+ required: ["memories"],
533
+ },
534
+ execute: async (
535
+ _id,
536
+ args: { memories: Array<{ content: string; metadata?: Record<string, string> }> },
537
+ ) => {
538
+ try {
539
+ // Auto-tag each memory with sender identity from tool context
540
+ if (ctx.requesterSenderId) {
541
+ for (const mem of args.memories) {
542
+ const metadata = mem.metadata || {};
543
+ if (!metadata.sender_id) {
544
+ metadata.sender_id = ctx.requesterSenderId;
545
+ }
546
+ mem.metadata = metadata;
547
+ }
548
+ }
549
+
550
+ const result = await client.batchStore(args.memories);
551
+ return {
552
+ content: [
553
+ {
554
+ type: "text",
555
+ text: `Batch stored ${args.memories.length} memories successfully.`,
556
+ },
557
+ ],
558
+ details: { count: args.memories.length, result },
559
+ };
560
+ } catch (err) {
561
+ return {
562
+ content: [{ type: "text", text: `Batch store failed: ${String(err)}` }],
563
+ details: { error: String(err) },
564
+ };
565
+ }
566
+ },
567
+ }),
568
+ { name: "memory_batch_store" },
569
+ );
570
+ }
571
+
572
+ // --------------------------------------------------------------------------
573
+ // 8. memory_context
574
+ // --------------------------------------------------------------------------
575
+ if (isToolEnabled("memory_context")) {
576
+ api.registerTool((ctx) => ({
577
+
578
+ name: "memory_context",
579
+ description:
580
+ "Build a context window from relevant memories, optimized for injecting into agent prompts with token budget awareness." +
581
+ (defaultProject ? ` Project defaults to '${defaultProject}' if not specified.` : ""),
582
+ parameters: {
583
+ type: "object",
584
+ properties: {
585
+ query: {
586
+ type: "string",
587
+ description: "The query to build context around.",
588
+ },
589
+ limit: {
590
+ type: "number",
591
+ description: "Maximum number of memories to include.",
592
+ },
593
+ threshold: {
594
+ type: "number",
595
+ description: "Minimum similarity threshold (0-1).",
596
+ },
597
+ max_tokens: {
598
+ type: "number",
599
+ description: "Maximum token budget for the context.",
600
+ },
601
+ project: {
602
+ type: "string",
603
+ description: "Project slug to scope the context.",
604
+ },
605
+ },
606
+ required: ["query"],
607
+ },
608
+ execute: async (
609
+ _id,
610
+ args: { query: string; limit?: number; threshold?: number; max_tokens?: number; project?: string },
611
+ ) => {
612
+ try {
613
+ const project = args.project ?? defaultProject;
614
+ const result = await client.buildContext(
615
+ args.query,
616
+ args.limit,
617
+ args.threshold,
618
+ args.max_tokens,
619
+ project,
620
+ );
621
+ return {
622
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
623
+ details: { result },
624
+ };
625
+ } catch (err) {
626
+ return {
627
+ content: [{ type: "text", text: `Context build failed: ${String(err)}` }],
628
+ details: { error: String(err) },
629
+ };
630
+ }
631
+ },
632
+ }),
633
+ { name: "memory_context" },
634
+ );
635
+ }
636
+
637
+ // --------------------------------------------------------------------------
638
+ // 9. memory_promote
639
+ // --------------------------------------------------------------------------
640
+ if (isToolEnabled("memory_promote")) {
641
+ api.registerTool((ctx) => ({
642
+
643
+ name: "memory_promote",
644
+ description:
645
+ "Promote a memory by updating its importance score and/or tier. Use to ensure critical memories are retained longer.",
646
+ parameters: {
647
+ type: "object",
648
+ properties: {
649
+ memory_id: {
650
+ type: "string",
651
+ description: "The memory ID to promote.",
652
+ },
653
+ importance: {
654
+ type: "number",
655
+ description: "New importance score (0-1).",
656
+ minimum: 0,
657
+ maximum: 1,
658
+ },
659
+ tier: {
660
+ type: "string",
661
+ description: "Target tier: hot, warm, or cold.",
662
+ enum: ["hot", "warm", "cold"],
663
+ },
664
+ },
665
+ required: ["memory_id", "importance"],
666
+ },
667
+ execute: async (_id, args: { memory_id: string; importance: number; tier?: string }) => {
668
+ try {
669
+ const result = await client.promote(args.memory_id, args.importance, args.tier);
670
+ return {
671
+ content: [
672
+ {
673
+ type: "text",
674
+ text: `Memory ${args.memory_id.slice(0, 8)}... promoted (importance: ${args.importance}${args.tier ? `, tier: ${args.tier}` : ""}).`,
675
+ },
676
+ ],
677
+ details: { result },
678
+ };
679
+ } catch (err) {
680
+ return {
681
+ content: [{ type: "text", text: `Promote failed: ${String(err)}` }],
682
+ details: { error: String(err) },
683
+ };
684
+ }
685
+ },
686
+ }),
687
+ { name: "memory_promote" },
688
+ );
689
+ }
690
+ }