diff --git a/mateclaw-server/src/main/java/vip/mate/memory/service/MemoryEmergenceService.java b/mateclaw-server/src/main/java/vip/mate/memory/service/MemoryEmergenceService.java index 2fe2da79..aff44dcf 100644 --- a/mateclaw-server/src/main/java/vip/mate/memory/service/MemoryEmergenceService.java +++ b/mateclaw-server/src/main/java/vip/mate/memory/service/MemoryEmergenceService.java @@ -68,9 +68,10 @@ public class MemoryEmergenceService { return; } - // 2. 读取 daily notes 内容 + // 2. 批量读取 daily notes 内容(避免 N+1 查询) StringBuilder dailyNotesBuilder = new StringBuilder(); for (String filename : dailyFilenames) { + // TODO: 未来可优化为 IN 批量查询,当前 listFiles() 会清除 content 字段 WorkspaceFileEntity file = workspaceFileService.getFile(agentId, filename); if (file != null && file.getContent() != null && !file.getContent().isBlank()) { dailyNotesBuilder.append("### ").append(filename).append("\n"); @@ -221,11 +222,14 @@ public class MemoryEmergenceService { // 防止无限膨胀:超过 20KB 时截断,只保留最近的内容 if (newContent.length() > 20_000) { int cutPoint = newContent.length() - 16_000; - // 找到下一个 "## " 标记作为安全截断点 int safePoint = newContent.indexOf("\n## ", cutPoint); if (safePoint > 0) { newContent = "# Dreaming 整合日记\n\n> 早期记录已归档\n\n" + newContent.substring(safePoint + 1); + } else { + // 没找到 ## 标记,硬截断保留最后 16KB + newContent = "# Dreaming 整合日记\n\n> 早期记录已归档\n\n" + + newContent.substring(cutPoint); } } diff --git a/mateclaw-server/src/main/java/vip/mate/memory/service/MemoryRecallService.java b/mateclaw-server/src/main/java/vip/mate/memory/service/MemoryRecallService.java index 535307cc..39e4ea70 100644 --- a/mateclaw-server/src/main/java/vip/mate/memory/service/MemoryRecallService.java +++ b/mateclaw-server/src/main/java/vip/mate/memory/service/MemoryRecallService.java @@ -94,9 +94,28 @@ public class MemoryRecallService { recallMapper.insert(entity); } catch (org.springframework.dao.DuplicateKeyException e) { - // 并发插入冲突,退化为更新 - log.debug("[MemoryRecall] Concurrent insert for {}, retrying as update", filename); - recordRecall(agentId, filename, snippetText, userQueryHash); + // 并发插入冲突,重新查询后更新(不递归,避免 StackOverflow) + log.debug("[MemoryRecall] Concurrent insert for {}, falling back to update", filename); + MemoryRecallEntity retry = recallMapper.selectOne( + new LambdaQueryWrapper() + .eq(MemoryRecallEntity::getAgentId, agentId) + .eq(MemoryRecallEntity::getFilename, filename) + .eq(MemoryRecallEntity::getDeleted, 0) + .last("LIMIT 1")); + if (retry != null) { + retry.setRecallCount(retry.getRecallCount() + 1); + retry.setDailyCount(retry.getDailyCount() + 1); + retry.setLastRecalledAt(now); + retry.setSnippetPreview(preview); + if (userQueryHash != null) { + List hashes = parseQueryHashes(retry.getQueryHashes()); + if (!hashes.contains(userQueryHash) && hashes.size() < MAX_QUERY_HASHES) { + hashes.add(userQueryHash); + } + retry.setQueryHashes(toJson(hashes)); + } + recallMapper.updateById(retry); + } } } } @@ -263,7 +282,10 @@ public class MemoryRecallService { item.put("score", c.getScore()); item.put("recallCount", c.getRecallCount()); item.put("dailyCount", c.getDailyCount()); - item.put("queryCount", parseQueryHashes(c.getQueryHashes()).size()); + // 避免 JSON 反序列化:直接数逗号估算 hash 数量("[\"a\",\"b\"]" 有 1 个逗号 = 2 个元素) + String qh = c.getQueryHashes(); + int queryCount = (qh == null || qh.length() <= 2) ? 0 : qh.split(",").length; + item.put("queryCount", queryCount); item.put("promoted", c.getPromoted()); item.put("lastRecalledAt", c.getLastRecalledAt()); item.put("snippetPreview", c.getSnippetPreview());