From b8a8aac7f93658377178c36848ae3cbee3cf0d25 Mon Sep 17 00:00:00 2001 From: matevip Date: Thu, 13 Aug 2026 03:08:44 -0400 Subject: [PATCH] fix: preserve agent id precision in memory tools --- .../memory/tool/StructuredMemoryTool.java | 2 +- .../tool/builtin/WorkspaceMemoryTool.java | 10 ++--- ...ructuredMemoryToolIdSerializationTest.java | 34 +++++++++++++++ ...orkspaceMemoryToolIdSerializationTest.java | 36 ++++++++++++++++ .../chat/__tests__/streamRequestBody.test.ts | 15 +++++++ mateclaw-ui/src/composables/chat/useChat.ts | 41 ++++++++++--------- 6 files changed, 113 insertions(+), 25 deletions(-) create mode 100644 mateclaw-server/src/test/java/vip/mate/memory/tool/StructuredMemoryToolIdSerializationTest.java create mode 100644 mateclaw-server/src/test/java/vip/mate/tool/builtin/WorkspaceMemoryToolIdSerializationTest.java create mode 100644 mateclaw-ui/src/composables/chat/__tests__/streamRequestBody.test.ts diff --git a/mateclaw-server/src/main/java/vip/mate/memory/tool/StructuredMemoryTool.java b/mateclaw-server/src/main/java/vip/mate/memory/tool/StructuredMemoryTool.java index be30b600..24d638ea 100644 --- a/mateclaw-server/src/main/java/vip/mate/memory/tool/StructuredMemoryTool.java +++ b/mateclaw-server/src/main/java/vip/mate/memory/tool/StructuredMemoryTool.java @@ -111,7 +111,7 @@ public class StructuredMemoryTool { readOwner(toolContext)); JSONObject result = new JSONObject(); - result.set("agentId", agentId); + result.set("agentId", String.valueOf(agentId)); result.set("count", results.size()); result.set("entries", results); return JSONUtil.toJsonPrettyStr(result); diff --git a/mateclaw-server/src/main/java/vip/mate/tool/builtin/WorkspaceMemoryTool.java b/mateclaw-server/src/main/java/vip/mate/tool/builtin/WorkspaceMemoryTool.java index f6bce0ef..d46eae5a 100644 --- a/mateclaw-server/src/main/java/vip/mate/tool/builtin/WorkspaceMemoryTool.java +++ b/mateclaw-server/src/main/java/vip/mate/tool/builtin/WorkspaceMemoryTool.java @@ -87,7 +87,7 @@ public class WorkspaceMemoryTool { } JSONObject result = new JSONObject(); - result.set("agentId", agentId); + result.set("agentId", String.valueOf(agentId)); result.set("count", files.size()); result.set("files", items); return JSONUtil.toJsonPrettyStr(result); @@ -118,7 +118,7 @@ public class WorkspaceMemoryTool { memoryRecallTracker.trackActiveRetrieval(agentId, filename, content); JSONObject result = new JSONObject(); - result.set("agentId", agentId); + result.set("agentId", String.valueOf(agentId)); result.set("filename", file.getFilename()); result.set("enabled", Boolean.TRUE.equals(file.getEnabled())); result.set("fileSize", file.getFileSize()); @@ -154,7 +154,7 @@ public class WorkspaceMemoryTool { WorkspaceFileEntity saved = workspaceFileService.saveVisibleFile(agentId, filename, content != null ? content : "", ownerKey); JSONObject result = new JSONObject(); - result.set("agentId", agentId); + result.set("agentId", String.valueOf(agentId)); result.set("filename", saved.getFilename()); result.set("created", before == null); result.set("overwritten", before != null); @@ -222,7 +222,7 @@ public class WorkspaceMemoryTool { WorkspaceFileEntity saved = workspaceFileService.saveVisibleFile(agentId, filename, updated, ownerKey); JSONObject result = new JSONObject(); - result.set("agentId", agentId); + result.set("agentId", String.valueOf(agentId)); result.set("filename", filename); result.set("replacements", replacements); result.set("replaceAll", replaceAllFlag); @@ -296,7 +296,7 @@ public class WorkspaceMemoryTool { hitsJson.add(h); } JSONObject result = new JSONObject(); - result.set("agentId", agentId); + result.set("agentId", String.valueOf(agentId)); result.set("query", trimmed); result.set("scope", scope == null || scope.isBlank() ? "all" : scope); result.set("totalHits", hits.size()); diff --git a/mateclaw-server/src/test/java/vip/mate/memory/tool/StructuredMemoryToolIdSerializationTest.java b/mateclaw-server/src/test/java/vip/mate/memory/tool/StructuredMemoryToolIdSerializationTest.java new file mode 100644 index 00000000..8fa66cdf --- /dev/null +++ b/mateclaw-server/src/test/java/vip/mate/memory/tool/StructuredMemoryToolIdSerializationTest.java @@ -0,0 +1,34 @@ +package vip.mate.memory.tool; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import vip.mate.memory.MemoryProperties; +import vip.mate.memory.identity.MemoryOwnerResolver; +import vip.mate.memory.service.StructuredMemoryService; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class StructuredMemoryToolIdSerializationTest { + + @Test + @DisplayName("recall_structured returns agentId as a JSON string to preserve snowflake precision") + void recallStructuredSerializesAgentIdAsString() { + StructuredMemoryService service = mock(StructuredMemoryService.class); + when(service.recall(anyLong(), nullable(String.class), nullable(String.class), anyString())) + .thenReturn(List.of()); + StructuredMemoryTool tool = new StructuredMemoryTool( + service, + new MemoryOwnerResolver(), + new MemoryProperties()); + + String json = tool.recall_structured(2079862124134313986L, "reference", "meeting", null); + + assertThat(json).contains("\"agentId\": \"2079862124134313986\""); + assertThat(json).doesNotContain("\"agentId\": 2079862124134313986"); + } +} diff --git a/mateclaw-server/src/test/java/vip/mate/tool/builtin/WorkspaceMemoryToolIdSerializationTest.java b/mateclaw-server/src/test/java/vip/mate/tool/builtin/WorkspaceMemoryToolIdSerializationTest.java new file mode 100644 index 00000000..d1e052dd --- /dev/null +++ b/mateclaw-server/src/test/java/vip/mate/tool/builtin/WorkspaceMemoryToolIdSerializationTest.java @@ -0,0 +1,36 @@ +package vip.mate.tool.builtin; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import vip.mate.memory.MemoryProperties; +import vip.mate.memory.identity.MemoryOwnerResolver; +import vip.mate.memory.service.MemoryRecallTracker; +import vip.mate.workspace.document.WorkspaceFileService; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class WorkspaceMemoryToolIdSerializationTest { + + @Test + @DisplayName("search_workspace_memory returns agentId as a JSON string to preserve snowflake precision") + void searchWorkspaceMemorySerializesAgentIdAsString() { + WorkspaceFileService files = mock(WorkspaceFileService.class); + when(files.searchSnippets(anyLong(), anyString(), anySet(), anyInt(), anyString())) + .thenReturn(List.of()); + WorkspaceMemoryTool tool = new WorkspaceMemoryTool( + files, + mock(MemoryRecallTracker.class), + new MemoryOwnerResolver(), + new MemoryProperties()); + + String json = tool.search_workspace_memory(2079862124134313986L, "meeting", "all", 10, null); + + assertThat(json).contains("\"agentId\": \"2079862124134313986\""); + assertThat(json).doesNotContain("\"agentId\": 2079862124134313986"); + } +} diff --git a/mateclaw-ui/src/composables/chat/__tests__/streamRequestBody.test.ts b/mateclaw-ui/src/composables/chat/__tests__/streamRequestBody.test.ts new file mode 100644 index 00000000..4087358d --- /dev/null +++ b/mateclaw-ui/src/composables/chat/__tests__/streamRequestBody.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from 'vitest' +import { buildChatStreamRequestBody } from '../useChat' + +describe('buildChatStreamRequestBody', () => { + it('serializes agentId as a string before JSON.stringify touches the request body', () => { + const body = buildChatStreamRequestBody('', { + conversationId: 'wecom:2079870010935783426:DeBaDe', + agentId: '2079862124134313986', + contentParts: [], + }) + + expect(body.agentId).toBe('2079862124134313986') + expect(JSON.stringify(body)).toContain('"agentId":"2079862124134313986"') + }) +}) diff --git a/mateclaw-ui/src/composables/chat/useChat.ts b/mateclaw-ui/src/composables/chat/useChat.ts index 92d5fafa..091aee87 100644 --- a/mateclaw-ui/src/composables/chat/useChat.ts +++ b/mateclaw-ui/src/composables/chat/useChat.ts @@ -193,6 +193,26 @@ export interface SendMessageOptions { regenerate?: boolean } +export function buildChatStreamRequestBody(content: string, options: SendMessageOptions): Record { + const body: Record = { + agentId: String(options.agentId), + message: content, + conversationId: options.conversationId, + contentParts: options.contentParts || [], + } + if (options.thinkingLevel) { + body.thinkingLevel = options.thinkingLevel + } + if (options.modelProvider && options.modelName) { + body.modelProvider = options.modelProvider + body.modelName = options.modelName + } + if (options.regenerate) { + body.regenerate = true + } + return body +} + export function useChat(options: UseChatOptions): UseChatReturn { const { baseUrl, token, onStreamEnd } = options const thinkingLevelRef = options.thinkingLevel @@ -2027,24 +2047,7 @@ export function useChat(options: UseChatOptions): UseChatReturn { currentAssistantId.value = assistantMessage.id as string // contentParts already includes file entries from buildOutgoingParts — do not re-merge attachments - const body: Record = { - agentId, - message: content, - conversationId, - contentParts, - } - if (options.thinkingLevel) { - body.thinkingLevel = options.thinkingLevel - } - // Per-conversation model: the backend pins it onto the conversation row - // so switching the model here never leaks into other conversations. - if (options.modelProvider && options.modelName) { - body.modelProvider = options.modelProvider - body.modelName = options.modelName - } - if (options.regenerate) { - body.regenerate = true - } + const body = buildChatStreamRequestBody(content, { ...options, contentParts }) await stream.connect(body) } catch (e) { error.value = e instanceof Error ? e : new Error(String(e)) @@ -2071,7 +2074,7 @@ export function useChat(options: UseChatOptions): UseChatReturn { method: 'POST', body: JSON.stringify({ message: content, - agentId, + agentId: String(agentId), contentParts: options.contentParts || [], }), })