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 6cf287f7..df10190d 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 @@ -35,6 +35,22 @@ public class MemoryRecallService { private static final int MAX_QUERY_HASHES = 32; + /** mate_memory_recall.filename is VARCHAR(256). Bound the value here so an + * over-long section key (path + '#' + H2 heading slug, see #461) can never + * blow past the column. Truncating at the entry point keeps the select / + * insert / update branches below operating on the same value, so the + * dup-key concurrency fallback still matches. */ + static final int MAX_FILENAME_LENGTH = 255; + + /** Truncate {@code filename} to {@link #MAX_FILENAME_LENGTH}. Package-private + * for direct unit testing. CJK chars live in the BMP, so {@code substring} + * cannot split a surrogate pair. */ + static String truncateFilename(String filename) { + return filename.length() > MAX_FILENAME_LENGTH + ? filename.substring(0, MAX_FILENAME_LENGTH) + : filename; + } + /** * 记录一次文件召回 */ @@ -42,6 +58,9 @@ public class MemoryRecallService { if (agentId == null || filename == null || filename.isBlank()) { return; } + // 写库前硬截断:覆盖所有调用路径(含 trackActiveRetrieval 透传的外部 filename), + // 防 filename 突破 VARCHAR(256) 导致写入失败(#461) + filename = truncateFilename(filename); // snippet preview 只取前 200 字符(避免对大文件做完整 SHA-256) String preview = snippetText != null && snippetText.length() > 200 diff --git a/mateclaw-server/src/main/java/vip/mate/memory/service/MemoryRecallTracker.java b/mateclaw-server/src/main/java/vip/mate/memory/service/MemoryRecallTracker.java index 0eaae68a..49449f20 100644 --- a/mateclaw-server/src/main/java/vip/mate/memory/service/MemoryRecallTracker.java +++ b/mateclaw-server/src/main/java/vip/mate/memory/service/MemoryRecallTracker.java @@ -126,12 +126,20 @@ public class MemoryRecallTracker { return count; } - private String sanitizeSectionKey(String heading) { + /** Max length of a section slug — keeps the full key (path + '#' + slug) + * well under the mate_memory_recall.filename VARCHAR(256) ceiling even + * when an LLM writes an over-long H2 heading. CJK chars live in the BMP, + * so {@code substring} can never split a surrogate pair here. */ + static final int MAX_SECTION_SLUG = 200; + + /** Package-private for direct unit testing of the slug/truncation logic. */ + static String sanitizeSectionKey(String heading) { // "## Some Title" -> "some-title" - return heading.replaceFirst("^#+\\s*", "") + String slug = heading.replaceFirst("^#+\\s*", "") .toLowerCase() .replaceAll("[^a-z0-9\\u4e00-\\u9fff]+", "-") .replaceAll("^-|-$", ""); + return slug.length() > MAX_SECTION_SLUG ? slug.substring(0, MAX_SECTION_SLUG) : slug; } /** diff --git a/mateclaw-server/src/main/resources/prompts/memory/summarize-system.txt b/mateclaw-server/src/main/resources/prompts/memory/summarize-system.txt index 9a2774cf..8ee986e7 100644 --- a/mateclaw-server/src/main/resources/prompts/memory/summarize-system.txt +++ b/mateclaw-server/src/main/resources/prompts/memory/summarize-system.txt @@ -42,7 +42,7 @@ MEMORY.md 与 PROFILE.md 会被**无条件注入每一次对话的系统提示** 字段说明: - `should_update`: 布尔值,是否有任何需要更新的内容。如果为 false,其余字段应为 null -- `daily_entry`: 字符串或 null。要追加到今日 daily note 的内容(markdown 格式,以时间戳开头如 "## HH:mm ...") +- `daily_entry`: 字符串或 null。要追加到今日 daily note 的内容(markdown 格式,以时间戳开头如 "## HH:mm 简要事件标题")。**二级标题(##)保持简短(不超过 30 字),只概括事件主题;事件细节、数字、过程写进标题下方的正文,不要堆进标题——过长的标题会导致下游索引截断。** - `memory_update`: 字符串或 null。MEMORY.md 的完整新内容(已合并现有内容,不是增量)。仅当有需要新增或修改的**跨项目稳定**信息时才填写 - `profile_update`: 字符串或 null。PROFILE.md 的完整新内容(已合并现有内容,不是增量)。仅当用户身份/偏好有显著变化时才填写 - `structured_entries`: 数组或 null。把适合按条目检索的**具体事实**路由到结构化记忆,每个元素形如 `{"type": "...", "key": "...", "content": "..."}`: diff --git a/mateclaw-server/src/test/java/vip/mate/memory/service/MemoryRecallFilenameTruncationTest.java b/mateclaw-server/src/test/java/vip/mate/memory/service/MemoryRecallFilenameTruncationTest.java new file mode 100644 index 00000000..67a50524 --- /dev/null +++ b/mateclaw-server/src/test/java/vip/mate/memory/service/MemoryRecallFilenameTruncationTest.java @@ -0,0 +1,111 @@ +package vip.mate.memory.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Guards the {@code mate_memory_recall.filename} VARCHAR(256) ceiling against + * over-long section keys (file path + '#' + a long H2 heading slug). See #461. + *
+ * Two boundaries are covered as pure functions, no Spring context needed: + *