@memorystack/clawdbot-memorystack 1.0.0 → 1.1.0

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/hooks/capture.ts CHANGED
@@ -18,7 +18,11 @@ function getLastTurn(messages: unknown[]): unknown[] {
18
18
  return lastUserIdx >= 0 ? messages.slice(lastUserIdx) : messages;
19
19
  }
20
20
 
21
- export function buildCaptureHandler(cfg: MemorystackConfig) {
21
+ export function buildCaptureHandler(
22
+ cfg: MemorystackConfig,
23
+ getSessionKey?: () => string | undefined,
24
+ getContext?: () => Record<string, any>,
25
+ ) {
22
26
  return async (event: Record<string, unknown>) => {
23
27
  if (
24
28
  !event.success ||
@@ -72,6 +76,8 @@ export function buildCaptureHandler(cfg: MemorystackConfig) {
72
76
  if (captured.length === 0) return;
73
77
 
74
78
  const content = captured.join("\n\n");
79
+ const sessionKey = getSessionKey?.();
80
+ const ctx = getContext?.() || {};
75
81
 
76
82
  log.debug(
77
83
  `capturing ${captured.length} texts (${content.length} chars)`,
@@ -84,8 +90,28 @@ export function buildCaptureHandler(cfg: MemorystackConfig) {
84
90
  enableLogging: cfg.debug,
85
91
  });
86
92
 
93
+ // Prepare metadata from context
94
+ const metadata: Record<string, any> = {
95
+ source: "clawdbot_auto_capture",
96
+ session_id: sessionKey,
97
+ timestamp: new Date().toISOString(),
98
+ user_id: ctx.userId,
99
+ agent_id: ctx.agentId,
100
+ provider: ctx.Provider,
101
+ model: ctx.Model,
102
+ sender_name: ctx.SenderName,
103
+ };
104
+
105
+ // Remove undefined values
106
+ Object.keys(metadata).forEach(key => metadata[key] === undefined && delete metadata[key]);
107
+
87
108
  await client.add(content, {
88
- metadata: { source: "clawdbot_auto_capture", timestamp: new Date().toISOString() },
109
+ sessionId: sessionKey,
110
+ agentId: ctx.agentId,
111
+ userId: ctx.userId,
112
+ teamId: ctx.teamId,
113
+ conversationId: ctx.conversationId,
114
+ metadata: metadata,
89
115
  });
90
116
 
91
117
  log.debug(`captured ${captured.length} messages successfully`);
package/index.ts CHANGED
@@ -4,9 +4,40 @@ import { initLogger } from "./logger.ts";
4
4
  import { registerSearchTool } from "./tools/search.ts";
5
5
  import { registerAddTool } from "./tools/add.ts";
6
6
  import { registerStatsTool } from "./tools/stats.ts";
7
+ import { registerDeleteTool } from "./tools/delete.ts";
8
+ import { registerReflectTool } from "./tools/reflect.ts";
9
+ import { registerConsolidateTool } from "./tools/consolidate.ts";
7
10
  import { buildRecallHandler } from "./hooks/recall.ts";
8
11
  import { buildCaptureHandler } from "./hooks/capture.ts";
9
12
 
13
+ // Helper to properly parse Moltbot session keys
14
+ // Format: "agent:{agentId}:{rest}" where rest may be "subagent:{uuid}" or "main" etc.
15
+ function parseSessionKey(sessionKey: string | undefined | null): {
16
+ agentId: string;
17
+ subagentId: string | null;
18
+ sessionId: string;
19
+ } {
20
+ const DEFAULT_AGENT = "main";
21
+ if (!sessionKey) return { agentId: DEFAULT_AGENT, subagentId: null, sessionId: "" };
22
+
23
+ const parts = sessionKey.split(":");
24
+ if (parts.length < 3 || parts[0] !== "agent") {
25
+ // Not a standard agent session key, return as-is
26
+ return { agentId: DEFAULT_AGENT, subagentId: null, sessionId: sessionKey };
27
+ }
28
+
29
+ const agentId = parts[1] || DEFAULT_AGENT;
30
+ const rest = parts.slice(2).join(":");
31
+
32
+ // Check if this is a subagent session
33
+ let subagentId: string | null = null;
34
+ if (rest.toLowerCase().startsWith("subagent:")) {
35
+ subagentId = rest.slice("subagent:".length);
36
+ }
37
+
38
+ return { agentId, subagentId, sessionId: sessionKey };
39
+ }
40
+
10
41
  export default {
11
42
  id: "clawdbot-memorystack",
12
43
  name: "MemoryStack",
@@ -24,20 +55,64 @@ export default {
24
55
 
25
56
  initLogger(api.logger, cfg.debug);
26
57
 
58
+ // Context tracking
59
+ let currentContext: Record<string, any> = {};
60
+ const getContext = () => currentContext;
61
+ const getSessionKey = () => currentContext.sessionKey;
62
+
27
63
  // Register tools
28
- registerSearchTool(api, cfg);
29
- registerAddTool(api, cfg);
64
+ registerSearchTool(api, cfg, getSessionKey, getContext);
65
+ registerAddTool(api, cfg, getSessionKey, getContext);
30
66
  registerStatsTool(api, cfg);
67
+ registerDeleteTool(api, cfg);
68
+ registerReflectTool(api, cfg);
69
+ registerConsolidateTool(api, cfg);
31
70
 
32
- // Auto-recall hook
33
- if (cfg.autoRecall) {
34
- const recallHandler = buildRecallHandler(cfg);
35
- api.on("before_agent_start", recallHandler);
36
- }
71
+ // ALWAYS capture context on before_agent_start (regardless of autoRecall setting)
72
+ // This ensures tools like memorystack_add have access to agentId, subagentId, etc.
73
+ const recallHandler = cfg.autoRecall ? buildRecallHandler(cfg, getSessionKey) : null;
74
+
75
+ api.on("before_agent_start", (event: Record<string, unknown>, ctx: Record<string, unknown>) => {
76
+ if (ctx) {
77
+ currentContext = { ...ctx };
78
+
79
+ // Parse session key to extract proper IDs
80
+ // Moltbot passes agentId incorrectly as "agent" (the literal prefix)
81
+ // We need to parse the sessionKey to get the actual agentId
82
+ const parsed = parseSessionKey(currentContext.sessionKey as string);
83
+
84
+ // Use parsed agentId, overriding the incorrect ctx.agentId
85
+ if (!currentContext.agentId || currentContext.agentId === "agent") {
86
+ currentContext.agentId = parsed.agentId;
87
+ }
88
+
89
+ // Track subagent ID separately if present (for scoped searches)
90
+ if (parsed.subagentId) {
91
+ currentContext.subagentId = parsed.subagentId;
92
+ }
93
+
94
+ // Normalize other commonly used fields
95
+ if (!currentContext.userId && currentContext.SenderId) currentContext.userId = currentContext.SenderId;
96
+
97
+ // Map group context to MemoryStack scoping
98
+ if (currentContext.groupId) currentContext.teamId = currentContext.groupId;
99
+ if (currentContext.groupChannel) currentContext.conversationId = currentContext.groupChannel;
100
+ // Fallback for conversation ID from session key if no group channel
101
+ if (!currentContext.conversationId && getSessionKey()) currentContext.conversationId = getSessionKey();
102
+ }
103
+
104
+ // Only run recall if autoRecall is enabled
105
+ if (recallHandler) {
106
+ return recallHandler(event);
107
+ }
108
+ });
37
109
 
38
110
  // Auto-capture hook
39
111
  if (cfg.autoCapture) {
40
- api.on("agent_end", buildCaptureHandler(cfg));
112
+ const captureHandler = buildCaptureHandler(cfg, getSessionKey, getContext);
113
+ api.on("agent_end", (event: Record<string, unknown>) => {
114
+ return captureHandler(event);
115
+ });
41
116
  }
42
117
 
43
118
  // Register service
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memorystack/clawdbot-memorystack",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "description": "Clawdbot MemoryStack memory plugin - long-term memory for your AI assistant",
6
6
  "license": "MIT",
@@ -22,7 +22,7 @@
22
22
  "access": "public"
23
23
  },
24
24
  "dependencies": {
25
- "@memorystack/sdk": "^1.0.3",
25
+ "@memorystack/sdk": "^1.0.4",
26
26
  "@sinclair/typebox": "0.34.47"
27
27
  },
28
28
  "peerDependencies": {
package/tools/add.ts CHANGED
@@ -7,21 +7,51 @@ import { log } from "../logger.ts";
7
7
  export function registerAddTool(
8
8
  api: ClawdbotPluginApi,
9
9
  cfg: MemorystackConfig,
10
+ getSessionKey?: () => string | undefined,
11
+ getContext?: () => Record<string, any>,
10
12
  ): void {
11
13
  api.registerTool(
12
14
  {
13
15
  name: "memorystack_add",
14
16
  label: "Memory Add",
15
- description: "Save important information to long-term memory with automatic importance scoring.",
17
+ description: `Save important information to long-term memory with automatic importance scoring.
18
+
19
+ **Automatic Context Capture**: The following are automatically captured from your session context:
20
+ - agent_id: Your agent identifier (e.g., "main", subagent UUID)
21
+ - session_id: Current session key for conversation tracking
22
+ - team_id: Team/group identifier (if in a group chat)
23
+ - conversation_id: Specific conversation thread ID
24
+ - user_id: The sender's ID (can be overridden with userId parameter)
25
+
26
+ You don't need to specify these manually - they are captured automatically to organize memories.`,
16
27
  parameters: Type.Object({
17
28
  text: Type.String({ description: "Information to remember" }),
18
- userId: Type.Optional(Type.String({ description: "User ID (for B2B apps)" })),
29
+ userId: Type.Optional(Type.String({ description: "Override user ID (defaults to sender)" })),
30
+ memory_type: Type.Optional(
31
+ Type.String({
32
+ description: "Type of memory: fact, preference, episode, procedure, belief",
33
+ }),
34
+ ),
19
35
  }),
20
36
  async execute(
21
37
  _toolCallId: string,
22
- params: { text: string; userId?: string },
38
+ params: { text: string; userId?: string; memory_type?: string },
23
39
  ) {
24
- log.debugRequest("add", { textLength: params.text.length, userId: params.userId });
40
+ const sessionKey = getSessionKey?.();
41
+ const ctx = getContext?.() || {};
42
+
43
+ // For subagents, use the subagent UUID as the agent_id
44
+ // This ensures subagent memories are properly attributed
45
+ const effectiveAgentId = ctx.subagentId || ctx.agentId || "main";
46
+
47
+ log.debugRequest("add", {
48
+ textLength: params.text.length,
49
+ userId: params.userId,
50
+ sessionKey,
51
+ agentId: ctx.agentId,
52
+ subagentId: ctx.subagentId,
53
+ effectiveAgentId,
54
+ });
25
55
 
26
56
  const client = new MemoryStackClient({
27
57
  apiKey: cfg.apiKey,
@@ -29,9 +59,30 @@ export function registerAddTool(
29
59
  enableLogging: cfg.debug,
30
60
  });
31
61
 
62
+ // Prepare metadata from context
63
+ const metadata: Record<string, any> = {
64
+ source: "clawdbot_tool",
65
+ session_key: sessionKey,
66
+ timestamp: new Date().toISOString(),
67
+ user_id: params.userId ?? ctx.userId,
68
+ parent_agent_id: ctx.agentId, // Track the parent agent
69
+ subagent_id: ctx.subagentId, // Track if this is a subagent
70
+ provider: ctx.Provider,
71
+ model: ctx.Model,
72
+ sender_name: ctx.SenderName,
73
+ };
74
+
75
+ // Remove undefined values
76
+ Object.keys(metadata).forEach(key => metadata[key] === undefined && delete metadata[key]);
77
+
32
78
  const result = await client.add(params.text, {
33
- userId: params.userId,
34
- metadata: { source: "clawdbot_tool" },
79
+ userId: params.userId ?? ctx.userId,
80
+ sessionId: sessionKey,
81
+ agentId: effectiveAgentId, // Use subagentId if available!
82
+ teamId: ctx.teamId,
83
+ conversationId: ctx.conversationId,
84
+ memory_type: params.memory_type,
85
+ metadata: metadata,
35
86
  });
36
87
 
37
88
  log.debugResponse("add", {
@@ -0,0 +1,116 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
3
+ import { MemoryStackClient } from "@memorystack/sdk";
4
+ import type { MemorystackConfig } from "../config.ts";
5
+ import { log } from "../logger.ts";
6
+
7
+ export function registerConsolidateTool(
8
+ api: ClawdbotPluginApi,
9
+ cfg: MemorystackConfig,
10
+ ): void {
11
+ api.registerTool(
12
+ {
13
+ name: "memorystack_consolidate",
14
+ label: "Consolidate Memories",
15
+ description:
16
+ "Merge duplicate or highly similar memories to reduce noise and keep memory clean. Useful for maintenance.",
17
+ parameters: Type.Object({
18
+ similarityThreshold: Type.Optional(
19
+ Type.Number({
20
+ description: "Similarity threshold 0-1 (default: 0.85). Higher = stricter matching.",
21
+ }),
22
+ ),
23
+ dryRun: Type.Optional(
24
+ Type.Boolean({
25
+ description: "Preview only, don't merge (default: false)",
26
+ }),
27
+ ),
28
+ maxPairs: Type.Optional(
29
+ Type.Number({
30
+ description: "Max pairs to process (default: 20)",
31
+ }),
32
+ ),
33
+ }),
34
+ async execute(
35
+ _toolCallId: string,
36
+ params: {
37
+ similarityThreshold?: number;
38
+ dryRun?: boolean;
39
+ maxPairs?: number;
40
+ },
41
+ ) {
42
+ const similarityThreshold = params.similarityThreshold ?? 0.85;
43
+ const dryRun = params.dryRun ?? false;
44
+ const maxPairs = params.maxPairs ?? 20;
45
+
46
+ log.debugRequest("consolidate", { similarityThreshold, dryRun, maxPairs });
47
+
48
+ const client = new MemoryStackClient({
49
+ apiKey: cfg.apiKey,
50
+ baseUrl: cfg.baseUrl,
51
+ enableLogging: cfg.debug,
52
+ });
53
+
54
+ try {
55
+ const result = await client.consolidateMemories({
56
+ similarityThreshold,
57
+ dryRun,
58
+ });
59
+
60
+ log.debugResponse("consolidate", {
61
+ merged: result.memoriesMerged ?? 0,
62
+ processed: result.memoriesProcessed ?? 0,
63
+ });
64
+
65
+ // Format output
66
+ const lines: string[] = [];
67
+ lines.push(`# 🧹 Memory Consolidation`);
68
+ lines.push("");
69
+ lines.push(`**Memories Processed:** ${result.memoriesProcessed ?? 0}`);
70
+ lines.push(`**Duplicates Merged:** ${result.memoriesMerged ?? 0}`);
71
+ lines.push(`**Memories Removed:** ${result.memoriesRemoved ?? 0}`);
72
+
73
+ if (result.mergedPairs && result.mergedPairs.length > 0) {
74
+ lines.push("");
75
+ lines.push("## Merged Pairs");
76
+ for (const pair of result.mergedPairs.slice(0, 5)) {
77
+ lines.push(`- Merged: "${pair.original?.substring(0, 50)}..." → "${pair.merged?.substring(0, 50)}..."`);
78
+ }
79
+ }
80
+
81
+ if (dryRun) {
82
+ lines.push("");
83
+ lines.push("*This was a dry run - no changes were made.*");
84
+ } else if (result.memoriesMerged === 0) {
85
+ lines.push("");
86
+ lines.push("✨ No duplicates found - your memory is already clean!");
87
+ } else {
88
+ lines.push("");
89
+ lines.push("✅ Consolidation complete!");
90
+ }
91
+
92
+ return {
93
+ content: [
94
+ {
95
+ type: "text" as const,
96
+ text: lines.join("\n"),
97
+ },
98
+ ],
99
+ details: result,
100
+ };
101
+ } catch (error: any) {
102
+ log.error("consolidate failed", error);
103
+ return {
104
+ content: [
105
+ {
106
+ type: "text" as const,
107
+ text: `❌ Consolidation failed: ${error?.message || "Unknown error"}`,
108
+ },
109
+ ],
110
+ };
111
+ }
112
+ },
113
+ },
114
+ { name: "memorystack_consolidate" },
115
+ );
116
+ }
@@ -0,0 +1,68 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
3
+ import { MemoryStackClient } from "@memorystack/sdk";
4
+ import type { MemorystackConfig } from "../config.ts";
5
+ import { log } from "../logger.ts";
6
+
7
+ export function registerDeleteTool(
8
+ api: ClawdbotPluginApi,
9
+ cfg: MemorystackConfig,
10
+ ): void {
11
+ api.registerTool(
12
+ {
13
+ name: "memorystack_delete",
14
+ label: "Delete Memory",
15
+ description:
16
+ "Delete a memory by ID. Use search first to find the memory ID if you only have the content.",
17
+ parameters: Type.Object({
18
+ memoryId: Type.String({ description: "UUID of the memory to delete" }),
19
+ hard: Type.Optional(
20
+ Type.Boolean({
21
+ description: "Permanently delete (true) or soft delete (false, default)",
22
+ }),
23
+ ),
24
+ }),
25
+ async execute(
26
+ _toolCallId: string,
27
+ params: { memoryId: string; hard?: boolean },
28
+ ) {
29
+ log.debugRequest("delete", { memoryId: params.memoryId, hard: params.hard });
30
+
31
+ const client = new MemoryStackClient({
32
+ apiKey: cfg.apiKey,
33
+ baseUrl: cfg.baseUrl,
34
+ enableLogging: cfg.debug,
35
+ });
36
+
37
+ try {
38
+ const result = await client.deleteMemory(params.memoryId, params.hard ?? false);
39
+
40
+ log.debugResponse("delete", { success: result.success });
41
+
42
+ return {
43
+ content: [
44
+ {
45
+ type: "text" as const,
46
+ text: result.success
47
+ ? `✅ Memory deleted successfully (ID: ${params.memoryId})`
48
+ : `❌ Failed to delete memory`,
49
+ },
50
+ ],
51
+ details: result,
52
+ };
53
+ } catch (error: any) {
54
+ log.error("delete failed", error);
55
+ return {
56
+ content: [
57
+ {
58
+ type: "text" as const,
59
+ text: `❌ Failed to delete memory: ${error?.message || "Unknown error"}`,
60
+ },
61
+ ],
62
+ };
63
+ }
64
+ },
65
+ },
66
+ { name: "memorystack_delete" },
67
+ );
68
+ }
@@ -0,0 +1,119 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
3
+ import { MemoryStackClient } from "@memorystack/sdk";
4
+ import type { MemorystackConfig } from "../config.ts";
5
+ import { log } from "../logger.ts";
6
+
7
+ export function registerReflectTool(
8
+ api: ClawdbotPluginApi,
9
+ cfg: MemorystackConfig,
10
+ ): void {
11
+ api.registerTool(
12
+ {
13
+ name: "memorystack_reflect",
14
+ label: "Reflect on Memories",
15
+ description:
16
+ "Analyze memories to discover patterns, generate insights, and identify recurring themes. Great for understanding user behavior over time.",
17
+ parameters: Type.Object({
18
+ timeWindowDays: Type.Optional(
19
+ Type.Number({
20
+ description: "Analyze memories from last N days (default: 7, max: 90)",
21
+ }),
22
+ ),
23
+ analysisDepth: Type.Optional(
24
+ Type.String({
25
+ description: "Analysis depth: 'shallow' (faster) or 'deep' (more thorough)",
26
+ }),
27
+ ),
28
+ dryRun: Type.Optional(
29
+ Type.Boolean({
30
+ description: "Preview only, don't save insights (default: false)",
31
+ }),
32
+ ),
33
+ }),
34
+ async execute(
35
+ _toolCallId: string,
36
+ params: {
37
+ timeWindowDays?: number;
38
+ analysisDepth?: string;
39
+ dryRun?: boolean;
40
+ },
41
+ ) {
42
+ const timeWindowDays = params.timeWindowDays ?? 7;
43
+ const analysisDepth = params.analysisDepth ?? "shallow";
44
+ const dryRun = params.dryRun ?? false;
45
+
46
+ log.debugRequest("reflect", { timeWindowDays, analysisDepth, dryRun });
47
+
48
+ const client = new MemoryStackClient({
49
+ apiKey: cfg.apiKey,
50
+ baseUrl: cfg.baseUrl,
51
+ enableLogging: cfg.debug,
52
+ });
53
+
54
+ try {
55
+ const result = await client.reflectOnMemories({
56
+ timeWindowDays,
57
+ analysisDepth: analysisDepth as "shallow" | "deep",
58
+ dryRun,
59
+ });
60
+
61
+ log.debugResponse("reflect", {
62
+ patterns: result.patterns?.length ?? 0,
63
+ insights: result.insightsGenerated ?? 0,
64
+ });
65
+
66
+ // Format output
67
+ const lines: string[] = [];
68
+ lines.push(`# 🔮 Memory Reflection`);
69
+ lines.push("");
70
+ lines.push(`**Memories Analyzed:** ${result.memoriesAnalyzed}`);
71
+ lines.push(`**Patterns Found:** ${result.patterns?.length ?? 0}`);
72
+ lines.push(`**Insights Generated:** ${result.insightsGenerated ?? 0}`);
73
+
74
+ if (result.patterns && result.patterns.length > 0) {
75
+ lines.push("");
76
+ lines.push("## Patterns");
77
+ for (const pattern of result.patterns.slice(0, 5)) {
78
+ lines.push(`- **${pattern.type || "Pattern"}:** ${pattern.description || pattern.content}`);
79
+ }
80
+ }
81
+
82
+ if (result.insights && result.insights.length > 0) {
83
+ lines.push("");
84
+ lines.push("## Insights");
85
+ for (const insight of result.insights.slice(0, 5)) {
86
+ lines.push(`- ${insight.content || insight}`);
87
+ }
88
+ }
89
+
90
+ if (dryRun) {
91
+ lines.push("");
92
+ lines.push("*This was a dry run - no insights were saved.*");
93
+ }
94
+
95
+ return {
96
+ content: [
97
+ {
98
+ type: "text" as const,
99
+ text: lines.join("\n"),
100
+ },
101
+ ],
102
+ details: result,
103
+ };
104
+ } catch (error: any) {
105
+ log.error("reflect failed", error);
106
+ return {
107
+ content: [
108
+ {
109
+ type: "text" as const,
110
+ text: `❌ Reflection failed: ${error?.message || "Unknown error"}`,
111
+ },
112
+ ],
113
+ };
114
+ }
115
+ },
116
+ },
117
+ { name: "memorystack_reflect" },
118
+ );
119
+ }
package/tools/search.ts CHANGED
@@ -7,18 +7,43 @@ import { log } from "../logger.ts";
7
7
  export function registerSearchTool(
8
8
  api: ClawdbotPluginApi,
9
9
  cfg: MemorystackConfig,
10
+ getSessionKey?: () => string | undefined,
11
+ getContext?: () => Record<string, any>,
10
12
  ): void {
11
13
  api.registerTool(
12
14
  {
13
15
  name: "memorystack_search",
14
16
  label: "Memory Search",
15
- description:
16
- "Search through long-term memories for relevant information using semantic search. Supports filtering by memory type, confidence, and recency.",
17
+ description: `Search through long-term memories using semantic search.
18
+
19
+ **Automatic Context**: Your agent_id, session_id, team_id, and conversation_id are available for filtering.
20
+
21
+ **Scoping Options**:
22
+ - scope="global": Search all memories (default)
23
+ - scope="agent": Search only memories from THIS agent
24
+ - scope="team": Search only memories from this team/group
25
+
26
+ **Explicit Filters** (override scope):
27
+ - agent_id: Search memories from a specific agent/subagent by UUID
28
+ - session_id: Search memories from a specific session
29
+ - metadata: Filter by metadata key-value pairs (e.g., parent_agent_id, source)
30
+
31
+ **Filtering**: Supports memory_type, min_confidence, days_ago, and current_session filters.`,
17
32
  parameters: Type.Object({
18
33
  query: Type.String({ description: "Search query" }),
19
34
  limit: Type.Optional(
20
35
  Type.Number({ description: "Max results (default: 5)" }),
21
36
  ),
37
+ agent_id: Type.Optional(
38
+ Type.String({
39
+ description: "Filter by specific agent/subagent UUID (e.g., from a spawned subagent)",
40
+ }),
41
+ ),
42
+ session_id: Type.Optional(
43
+ Type.String({
44
+ description: "Filter by specific session key",
45
+ }),
46
+ ),
22
47
  memory_type: Type.Optional(
23
48
  Type.String({
24
49
  description:
@@ -35,19 +60,47 @@ export function registerSearchTool(
35
60
  description: "Only memories from last N days",
36
61
  }),
37
62
  ),
63
+ current_session: Type.Optional(
64
+ Type.Boolean({
65
+ description: "Filter to current session only (default: false)",
66
+ }),
67
+ ),
68
+ scope: Type.Optional(
69
+ Type.String({
70
+ description: "Scope of search: 'global', 'agent', 'team' (default: 'global')",
71
+ enum: ["global", "agent", "team"],
72
+ }),
73
+ ),
74
+ metadata: Type.Optional(
75
+ Type.Record(Type.String(), Type.Any(), {
76
+ description: "Filter by metadata fields (e.g., { parent_agent_id: 'main' })",
77
+ }),
78
+ ),
38
79
  }),
39
80
  async execute(
40
81
  _toolCallId: string,
41
82
  params: {
42
83
  query: string;
43
84
  limit?: number;
85
+ agent_id?: string;
86
+ session_id?: string;
44
87
  memory_type?: string;
45
88
  min_confidence?: number;
46
89
  days_ago?: number;
90
+ current_session?: boolean;
91
+ scope?: "global" | "agent" | "team";
92
+ metadata?: Record<string, any>;
47
93
  },
48
94
  ) {
49
95
  const limit = params.limit ?? 5;
50
- log.debugRequest("search", { query: params.query, limit, ...params });
96
+ const sessionKey = getSessionKey?.();
97
+ const ctx = getContext?.() || {};
98
+
99
+ log.debugRequest("search", {
100
+ query: params.query,
101
+ scope: params.scope,
102
+ agentId: ctx.agentId
103
+ });
51
104
 
52
105
  const client = new MemoryStackClient({
53
106
  apiKey: cfg.apiKey,
@@ -55,51 +108,102 @@ export function registerSearchTool(
55
108
  enableLogging: cfg.debug,
56
109
  });
57
110
 
58
- const searchOpts: Record<string, unknown> = { limit };
59
- if (params.memory_type) {
60
- searchOpts.memory_type = params.memory_type;
111
+ // Build search options
112
+ const searchOpts: any = {
113
+ limit,
114
+ userId: ctx.userId, // Default to current user
115
+ min_confidence: params.min_confidence,
116
+ memory_type: params.memory_type,
117
+ };
118
+
119
+ // For subagents, use subagentId as the effective agent
120
+ const effectiveAgentId = ctx.subagentId || ctx.agentId;
121
+
122
+ // Priority: explicit params > scope > default
123
+ // 1. Explicit agent_id/session_id params override everything
124
+ if (params.agent_id) {
125
+ searchOpts.agentId = params.agent_id;
126
+ } else if (params.scope === "agent" && effectiveAgentId) {
127
+ // 2. Scope-based filtering
128
+ searchOpts.agentId = effectiveAgentId;
129
+ } else if (params.scope === "team" && ctx.teamId) {
130
+ searchOpts.teamId = ctx.teamId;
131
+ }
132
+ // 3. global scope = no agent filter (default)
133
+
134
+ // Session filtering: explicit param > current_session flag
135
+ if (params.session_id) {
136
+ searchOpts.sessionId = params.session_id;
137
+ } else if (params.current_session && sessionKey) {
138
+ searchOpts.sessionId = sessionKey;
139
+ }
140
+
141
+ // Metadata filtering (passed directly to SDK)
142
+ if (params.metadata && Object.keys(params.metadata).length > 0) {
143
+ searchOpts.metadata = params.metadata;
144
+ }
145
+
146
+ // Apply Time Filter (handled via generic params if needed, or client-side filtering)
147
+ // improved SDK might support start_date, but for now we can rely on natural language query
148
+ // or pass it if backend supports it. The backend supports start_date.
149
+ if (params.days_ago) {
150
+ const date = new Date();
151
+ date.setDate(date.getDate() - params.days_ago);
152
+ searchOpts.start_date = date.toISOString();
61
153
  }
62
154
 
63
- const results = await client.search(params.query, searchOpts);
155
+ try {
156
+ const results = await client.search(params.query, searchOpts);
157
+
158
+ if (results.count === 0) {
159
+ return {
160
+ content: [
161
+ { type: "text" as const, text: "No relevant memories found." },
162
+ ],
163
+ };
164
+ }
165
+
166
+ const text = results.results
167
+ .map((r, i) => {
168
+ const type = r.memory_type ? ` [${r.memory_type}]` : "";
169
+ const conf = r.confidence
170
+ ? ` (${(r.confidence * 100).toFixed(0)}%)`
171
+ : "";
172
+ // Full ISO timestamp for temporal reasoning (e.g., 2024-06-03T14:30:00Z)
173
+ const timestamp = r.created_at ? ` [${r.created_at}]` : "";
174
+ return `${i + 1}. ${r.content}${type}${conf}${timestamp}`;
175
+ })
176
+ .join("\n");
177
+
178
+ log.debugResponse("search", { count: results.count });
64
179
 
65
- if (results.count === 0) {
66
180
  return {
67
181
  content: [
68
- { type: "text" as const, text: "No relevant memories found." },
182
+ {
183
+ type: "text" as const,
184
+ text: `Found ${results.count} memories:\n\n${text}`,
185
+ },
69
186
  ],
187
+ details: {
188
+ count: results.count,
189
+ mode: results.mode,
190
+ memories: results.results.map((r) => ({
191
+ id: r.id,
192
+ content: r.content,
193
+ memory_type: r.memory_type,
194
+ confidence: r.confidence,
195
+ created_at: r.created_at,
196
+ metadata: r.metadata,
197
+ })),
198
+ },
199
+ };
200
+ } catch (error: any) {
201
+ log.error("search failed", error);
202
+ return {
203
+ content: [{ type: "text", text: `Search failed: ${error.message}` }],
204
+ isError: true,
70
205
  };
71
206
  }
72
-
73
- const text = results.results
74
- .map((r, i) => {
75
- const type = r.memory_type ? ` [${r.memory_type}]` : "";
76
- const conf = r.confidence
77
- ? ` (${(r.confidence * 100).toFixed(0)}%)`
78
- : "";
79
- return `${i + 1}. ${r.content}${type}${conf}`;
80
- })
81
- .join("\n");
82
-
83
- log.debugResponse("search", { count: results.count });
84
-
85
- return {
86
- content: [
87
- {
88
- type: "text" as const,
89
- text: `Found ${results.count} memories:\n\n${text}`,
90
- },
91
- ],
92
- details: {
93
- count: results.count,
94
- mode: results.mode,
95
- memories: results.results.map((r) => ({
96
- id: r.id,
97
- content: r.content,
98
- memory_type: r.memory_type,
99
- confidence: r.confidence,
100
- })),
101
- },
102
- };
103
207
  },
104
208
  },
105
209
  { name: "memorystack_search" },
@@ -0,0 +1,52 @@
1
+ import { MemoryStackClient } from "@memorystack/sdk";
2
+
3
+ const API_KEY = "mem_live_KT9roc_L4O60KJ6_l7SGdzXeTib1RHoF3QlmhLghMbQ";
4
+
5
+ async function verifyRecentMemories() {
6
+ const client = new MemoryStackClient({
7
+ apiKey: API_KEY,
8
+ baseUrl: "http://localhost:3000",
9
+ });
10
+
11
+ console.log("🔍 Inspecting Most Recent Memories\n");
12
+ console.log("=".repeat(60) + "\n");
13
+
14
+ try {
15
+ // Fetch 5 most recent memories
16
+ const results = await client.search("memorystack", {
17
+ limit: 5,
18
+ });
19
+
20
+ console.log(`Found ${results.count} recent memories.\n`);
21
+
22
+ results.results.forEach((m, i) => {
23
+ console.log(`Memory #${i + 1}`);
24
+ console.log(` ID: ${m.id}`);
25
+ console.log(` Agent ID: "${m.agent_id}"`);
26
+ console.log(` Session ID: "${m.session_id}"`);
27
+ console.log(` Created At: ${m.created_at}`);
28
+ console.log(` Content: ${m.content.substring(0, 100)}...`);
29
+
30
+ // Check metadata for subagent details
31
+ if (m.metadata) {
32
+ console.log(` Metadata:`);
33
+ if (m.metadata.subagent_id) console.log(` - subagent_id: ${m.metadata.subagent_id}`);
34
+ if (m.metadata.parent_agent_id) console.log(` - parent_agent_id: ${m.metadata.parent_agent_id}`);
35
+ }
36
+
37
+ // Verification logic
38
+ if (m.agent_id !== "main" && m.agent_id !== "agent") {
39
+ console.log(` ✅ SUCCESS: Uses specific Agent ID!`);
40
+ } else if (m.metadata?.subagent_id) {
41
+ console.log(` ⚠️ Stored as 'main', but has subagent_id in metadata.`);
42
+ }
43
+
44
+ console.log("-".repeat(40));
45
+ });
46
+
47
+ } catch (e: any) {
48
+ console.log(`❌ Error: ${e.message}`);
49
+ }
50
+ }
51
+
52
+ verifyRecentMemories().catch(console.error);