mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(memory): Dream v2 Phase 1 engine — consolidate refactor, focused endpoint, monthly archive
Five-commit bundle brings the Dream v2 P1 engine layer online, sitting on top of the lifecycle mediator foundation already merged. B.1-B.4 · Schema + records - Flyway V26 (dream_report) + V27 (memory_recall review fields), both h2 and mysql - DreamReportEntity + DreamMode + DreamStatus enum + record types - DreamReportMapper repository layer B.5-B.8 · Consolidate refactor + focused dream - MemoryEmergenceService refactored for plug-in dream modes - MemoryRecallService extended with promoted/rejected review fields - Focused dream endpoint + prompt template - MemoryController exposes the review/trigger surface B.9-B.10 · Monthly archive service - MemoryArchiveService rolls cold promoted entries into archival rows and reclaims daily_count storage - DreamingScheduler runs archive job on its own schedule B.12-B.14 · Tests - MemoryArchiveServiceTest - DreamFlagGuardTest - DreamV2AcceptanceIT (end-to-end acceptance under feature flag) Plus a verification script + HTTP e2e kit in the private test/ dir, used for local staged rollout — not part of the open-source distribution. All features stay gated behind the mate.memory.dream.* flags from Phase 1. Enable per-phase after staging validation.
This commit is contained in:
parent
907c6eff8c
commit
70c90d814d
@ -0,0 +1,131 @@
|
||||
package vip.mate.memory.archive;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import vip.mate.memory.MemoryProperties;
|
||||
import vip.mate.workspace.document.WorkspaceFileService;
|
||||
import vip.mate.workspace.document.model.WorkspaceFileEntity;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Monthly archive service for DREAMS.md.
|
||||
* <p>
|
||||
* Moves entries older than archiveKeepDays to monthly archive files
|
||||
* at memory/dreams/YYYY-MM.md. Idempotent: already-archived entries
|
||||
* are not moved again.
|
||||
*
|
||||
* @author MateClaw Team
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MemoryArchiveService {
|
||||
|
||||
private final WorkspaceFileService workspaceFileService;
|
||||
private final MemoryProperties properties;
|
||||
|
||||
private static final Pattern DIARY_HEADER = Pattern.compile(
|
||||
"^## (\\d{4}-\\d{2}-\\d{2}) \\d{2}:\\d{2}.*$");
|
||||
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
|
||||
/**
|
||||
* Archive old entries from DREAMS.md to monthly files.
|
||||
* First line checks archiveEnabled flag.
|
||||
*/
|
||||
public void archiveOldDreams(Long agentId) {
|
||||
if (!properties.getDream().isArchiveEnabled()) return;
|
||||
|
||||
WorkspaceFileEntity dreamsFile = workspaceFileService.getFile(agentId, "DREAMS.md");
|
||||
if (dreamsFile == null || dreamsFile.getContent() == null || dreamsFile.getContent().isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String content = dreamsFile.getContent();
|
||||
LocalDate cutoff = LocalDate.now().minusDays(properties.getDream().getArchiveKeepDays());
|
||||
|
||||
// Split into sections by ## header
|
||||
String[] lines = content.split("\n");
|
||||
StringBuilder kept = new StringBuilder();
|
||||
Map<String, StringBuilder> archives = new LinkedHashMap<>(); // YYYY-MM -> content
|
||||
|
||||
StringBuilder currentSection = new StringBuilder();
|
||||
String currentDate = null;
|
||||
boolean inHeader = true; // first lines before any ## section
|
||||
|
||||
for (String line : lines) {
|
||||
Matcher m = DIARY_HEADER.matcher(line);
|
||||
if (m.matches()) {
|
||||
// Flush previous section
|
||||
flushSection(currentDate, currentSection, cutoff, kept, archives);
|
||||
currentDate = m.group(1);
|
||||
currentSection = new StringBuilder();
|
||||
currentSection.append(line).append("\n");
|
||||
inHeader = false;
|
||||
} else if (inHeader) {
|
||||
kept.append(line).append("\n");
|
||||
} else {
|
||||
currentSection.append(line).append("\n");
|
||||
}
|
||||
}
|
||||
// Flush last section
|
||||
flushSection(currentDate, currentSection, cutoff, kept, archives);
|
||||
|
||||
if (archives.isEmpty()) {
|
||||
log.debug("[Memory] No old dream entries to archive for agent={}", agentId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Write archive files
|
||||
for (Map.Entry<String, StringBuilder> entry : archives.entrySet()) {
|
||||
String archiveFilename = "memory/dreams/" + entry.getKey() + ".md";
|
||||
String existing = readSafe(agentId, archiveFilename);
|
||||
String archiveContent = existing.isBlank()
|
||||
? "# Dreaming Archive " + entry.getKey() + "\n\n" + entry.getValue()
|
||||
: existing + "\n" + entry.getValue();
|
||||
workspaceFileService.saveFile(agentId, archiveFilename, archiveContent);
|
||||
}
|
||||
|
||||
// Update DREAMS.md with only kept entries
|
||||
String newContent = kept.toString().trim();
|
||||
if (newContent.isEmpty()) {
|
||||
newContent = "# Dreaming 整合日记\n\n> All entries archived.";
|
||||
}
|
||||
workspaceFileService.saveFile(agentId, "DREAMS.md", newContent);
|
||||
|
||||
int archivedMonths = archives.size();
|
||||
log.info("[Memory] Archived dream entries to {} monthly files for agent={}", archivedMonths, agentId);
|
||||
}
|
||||
|
||||
private void flushSection(String dateStr, StringBuilder section, LocalDate cutoff,
|
||||
StringBuilder kept, Map<String, StringBuilder> archives) {
|
||||
if (dateStr == null || section.length() == 0) return;
|
||||
try {
|
||||
LocalDate date = LocalDate.parse(dateStr, DATE_FMT);
|
||||
if (date.isBefore(cutoff)) {
|
||||
String monthKey = dateStr.substring(0, 7); // YYYY-MM
|
||||
archives.computeIfAbsent(monthKey, k -> new StringBuilder()).append(section);
|
||||
} else {
|
||||
kept.append(section);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Unparseable date: keep it
|
||||
kept.append(section);
|
||||
}
|
||||
}
|
||||
|
||||
private String readSafe(Long agentId, String filename) {
|
||||
try {
|
||||
WorkspaceFileEntity file = workspaceFileService.getFile(agentId, filename);
|
||||
return file != null && file.getContent() != null ? file.getContent() : "";
|
||||
} catch (Exception e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7,9 +7,8 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import vip.mate.common.result.R;
|
||||
import vip.mate.workspace.core.annotation.RequireWorkspaceRole;
|
||||
import vip.mate.memory.service.MemoryEmergenceService;
|
||||
import vip.mate.memory.service.MemoryRecallService;
|
||||
import vip.mate.memory.service.MemorySummarizationService;
|
||||
import vip.mate.memory.MemoryProperties;
|
||||
import vip.mate.memory.service.*;
|
||||
import vip.mate.memory.scheduler.DreamingScheduler;
|
||||
import vip.mate.workspace.document.WorkspaceFileService;
|
||||
import vip.mate.workspace.document.model.WorkspaceFileEntity;
|
||||
@ -35,22 +34,44 @@ public class MemoryController {
|
||||
private final MemoryEmergenceService emergenceService;
|
||||
private final MemorySummarizationService summarizationService;
|
||||
private final MemoryRecallService recallService;
|
||||
private final MemoryProperties memoryProperties;
|
||||
private final DreamingScheduler dreamingScheduler;
|
||||
private final WorkspaceFileService workspaceFileService;
|
||||
|
||||
@Operation(summary = "手动触发记忆整合(daily notes → MEMORY.md)")
|
||||
@Operation(summary = "手动触发记忆整合(daily notes → MEMORY.md,NIGHTLY 模式)")
|
||||
@PostMapping("/{agentId}/emergence")
|
||||
@RequireWorkspaceRole("member")
|
||||
public R<Map<String, String>> triggerEmergence(@PathVariable Long agentId) {
|
||||
public R<DreamReport> triggerEmergence(@PathVariable Long agentId) {
|
||||
try {
|
||||
emergenceService.consolidate(agentId);
|
||||
return R.ok(Map.of("status", "completed"));
|
||||
DreamReport report = emergenceService.consolidate(agentId, DreamMode.NIGHTLY, null);
|
||||
return R.ok(report);
|
||||
} catch (Exception e) {
|
||||
log.error("[Memory] Manual emergence failed for agent={}: {}", agentId, e.getMessage(), e);
|
||||
return R.fail("记忆整合失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "Focused Dream — 围绕指定主题触发记忆整合")
|
||||
@PostMapping("/{agentId}/dreaming/focused")
|
||||
@RequireWorkspaceRole("member")
|
||||
public R<DreamReport> triggerFocusedDream(@PathVariable Long agentId,
|
||||
@RequestBody Map<String, String> body) {
|
||||
if (!memoryProperties.getDream().isFocusedEnabled()) {
|
||||
return R.fail(410, "Focused dream is disabled");
|
||||
}
|
||||
String topic = body != null ? body.get("topic") : null;
|
||||
if (topic == null || topic.isBlank()) {
|
||||
return R.fail("topic is required");
|
||||
}
|
||||
try {
|
||||
DreamReport report = emergenceService.consolidate(agentId, DreamMode.FOCUSED, topic);
|
||||
return R.ok(report);
|
||||
} catch (Exception e) {
|
||||
log.error("[Memory] Focused dream failed for agent={}: {}", agentId, e.getMessage(), e);
|
||||
return R.fail("Focused dream failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "手动触发对话记忆提取")
|
||||
@PostMapping("/{agentId}/summarize/{conversationId}")
|
||||
@RequireWorkspaceRole("member")
|
||||
|
||||
@ -0,0 +1,62 @@
|
||||
package vip.mate.memory.model;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Dream report entity — persists each dream consolidation run.
|
||||
*
|
||||
* @author MateClaw Team
|
||||
*/
|
||||
@Data
|
||||
@TableName("mate_dream_report")
|
||||
public class DreamReportEntity {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
|
||||
private Long agentId;
|
||||
|
||||
/** NIGHTLY | FOCUSED */
|
||||
private String mode;
|
||||
|
||||
/** Topic hint for FOCUSED mode; null for NIGHTLY */
|
||||
private String topic;
|
||||
|
||||
/** cron | user | api */
|
||||
private String triggerSource;
|
||||
|
||||
/** userId or "system" */
|
||||
private String triggeredBy;
|
||||
|
||||
private LocalDateTime startedAt;
|
||||
|
||||
private LocalDateTime finishedAt;
|
||||
|
||||
private Integer candidateCount;
|
||||
|
||||
private Integer promotedCount;
|
||||
|
||||
private Integer rejectedCount;
|
||||
|
||||
/** Diff between old and new MEMORY.md */
|
||||
private String memoryDiff;
|
||||
|
||||
/** LLM explanation (first 500 chars) */
|
||||
private String llmReason;
|
||||
|
||||
/** SUCCESS | FAILED | SKIPPED */
|
||||
private String status;
|
||||
|
||||
private String errorMessage;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
private Integer deleted;
|
||||
}
|
||||
@ -50,6 +50,12 @@ public class MemoryRecallEntity {
|
||||
/** 是否已提升到 MEMORY.md */
|
||||
private Boolean promoted;
|
||||
|
||||
/** Times this candidate was reviewed but not promoted (Dream v2, Phase 1 write-only) */
|
||||
private Integer reviewCount;
|
||||
|
||||
/** Last time this candidate was reviewed during a dream run */
|
||||
private LocalDateTime lastReviewedAt;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
package vip.mate.memory.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import vip.mate.memory.model.DreamReportEntity;
|
||||
|
||||
/**
|
||||
* Mapper for mate_dream_report table.
|
||||
*
|
||||
* @author MateClaw Team
|
||||
*/
|
||||
@Mapper
|
||||
public interface DreamReportMapper extends BaseMapper<DreamReportEntity> {
|
||||
}
|
||||
@ -52,7 +52,7 @@ public class DreamingScheduler {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
emergenceService.consolidate(agent.getId());
|
||||
emergenceService.consolidate(agent.getId(), vip.mate.memory.service.DreamMode.NIGHTLY, null);
|
||||
success++;
|
||||
} catch (Exception e) {
|
||||
failed++;
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
package vip.mate.memory.service;
|
||||
|
||||
/**
|
||||
* Dream consolidation modes. Phase 1: NIGHTLY + FOCUSED only.
|
||||
*
|
||||
* @author MateClaw Team
|
||||
*/
|
||||
public enum DreamMode {
|
||||
NIGHTLY,
|
||||
FOCUSED
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
package vip.mate.memory.service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Structured dream consolidation result returned by consolidate().
|
||||
*
|
||||
* @author MateClaw Team
|
||||
*/
|
||||
public record DreamReport(
|
||||
Long id,
|
||||
Long agentId,
|
||||
DreamMode mode,
|
||||
String topic,
|
||||
String triggerSource,
|
||||
String triggeredBy,
|
||||
LocalDateTime startedAt,
|
||||
LocalDateTime finishedAt,
|
||||
int candidateCount,
|
||||
int promotedCount,
|
||||
int rejectedCount,
|
||||
String memoryDiff,
|
||||
String llmReason,
|
||||
DreamStatus status,
|
||||
String errorMessage,
|
||||
List<PromotedEntry> promoted,
|
||||
List<RejectedEntry> rejected
|
||||
) {}
|
||||
@ -0,0 +1,12 @@
|
||||
package vip.mate.memory.service;
|
||||
|
||||
/**
|
||||
* Dream consolidation result status.
|
||||
*
|
||||
* @author MateClaw Team
|
||||
*/
|
||||
public enum DreamStatus {
|
||||
SUCCESS,
|
||||
FAILED,
|
||||
SKIPPED
|
||||
}
|
||||
@ -15,19 +15,21 @@ import vip.mate.agent.prompt.PromptLoader;
|
||||
import vip.mate.llm.service.ModelConfigService;
|
||||
import vip.mate.llm.model.ModelConfigEntity;
|
||||
import vip.mate.memory.MemoryProperties;
|
||||
import vip.mate.memory.model.DreamReportEntity;
|
||||
import vip.mate.memory.model.MemoryRecallEntity;
|
||||
import vip.mate.memory.repository.DreamReportMapper;
|
||||
import vip.mate.workspace.document.WorkspaceFileService;
|
||||
import vip.mate.workspace.document.model.WorkspaceFileEntity;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 记忆整合服务
|
||||
* Memory emergence (dream) service.
|
||||
* <p>
|
||||
* 读取近 N 天的 daily notes,提炼反复出现的模式和重要信息,
|
||||
* 合并到 MEMORY.md 中。
|
||||
* Reads daily notes + scored recall candidates, invokes LLM to consolidate
|
||||
* recurring patterns into MEMORY.md, and produces a structured DreamReport.
|
||||
*
|
||||
* @author MateClaw Team
|
||||
*/
|
||||
@ -42,19 +44,34 @@ public class MemoryEmergenceService {
|
||||
private final MemoryProperties properties;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final MemoryRecallService recallService;
|
||||
private final DreamReportMapper dreamReportMapper;
|
||||
private final vip.mate.memory.archive.MemoryArchiveService archiveService;
|
||||
|
||||
/**
|
||||
* 执行记忆整合:将 daily notes 中的重复模式提炼到 MEMORY.md
|
||||
* Legacy signature — delegates to NIGHTLY mode for backward compatibility.
|
||||
*/
|
||||
public DreamReport consolidate(Long agentId) {
|
||||
return consolidate(agentId, DreamMode.NIGHTLY, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute memory consolidation with the specified mode and optional topic.
|
||||
*
|
||||
* @param agentId Agent ID
|
||||
* @param mode NIGHTLY or FOCUSED
|
||||
* @param topic topic hint for FOCUSED mode (null for NIGHTLY)
|
||||
* @return structured DreamReport (never null)
|
||||
*/
|
||||
public void consolidate(Long agentId) {
|
||||
public DreamReport consolidate(Long agentId, DreamMode mode, String topic) {
|
||||
LocalDateTime startedAt = LocalDateTime.now();
|
||||
String triggerSource = mode == DreamMode.NIGHTLY ? "cron" : "user";
|
||||
|
||||
if (!properties.isEmergenceEnabled()) {
|
||||
log.debug("[Memory] Emergence is disabled, skipping for agent={}", agentId);
|
||||
return;
|
||||
return buildSkippedReport(agentId, mode, topic, triggerSource, startedAt, "emergence disabled");
|
||||
}
|
||||
|
||||
// 1. 列出所有 memory/*.md 文件
|
||||
// 1. Load daily notes
|
||||
List<WorkspaceFileEntity> allFiles = workspaceFileService.listFiles(agentId);
|
||||
List<String> dailyFilenames = allFiles.stream()
|
||||
.map(WorkspaceFileEntity::getFilename)
|
||||
@ -65,13 +82,11 @@ public class MemoryEmergenceService {
|
||||
|
||||
if (dailyFilenames.isEmpty()) {
|
||||
log.info("[Memory] No daily notes found for agent={}, skipping emergence", agentId);
|
||||
return;
|
||||
return buildSkippedReport(agentId, mode, topic, triggerSource, startedAt, "no 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");
|
||||
@ -82,41 +97,28 @@ public class MemoryEmergenceService {
|
||||
|
||||
if (dailyNotes.isEmpty()) {
|
||||
log.info("[Memory] All daily notes are empty for agent={}, skipping emergence", agentId);
|
||||
return;
|
||||
return buildSkippedReport(agentId, mode, topic, triggerSource, startedAt, "all daily notes empty");
|
||||
}
|
||||
|
||||
// 3. 读取现有 MEMORY.md
|
||||
String memoryContent = readFileContentSafe(agentId, "MEMORY.md");
|
||||
// 2. Read existing MEMORY.md (for diff later)
|
||||
String oldMemoryContent = readFileContentSafe(agentId, "MEMORY.md");
|
||||
|
||||
// 4. 计算召回评分(必须在 resetDailyCounts 之前,否则 velocity 信号被清零)
|
||||
// 3. Score candidates (must happen before resetDailyCounts)
|
||||
List<MemoryRecallEntity> scoredCandidates = recallService.computeScores(agentId);
|
||||
boolean hasScoredCandidates = !scoredCandidates.isEmpty();
|
||||
|
||||
// 5. 评分快照完成后再重置 dailyCount,为下一轮积累
|
||||
// 4. Reset daily counts for next accumulation cycle
|
||||
recallService.resetDailyCounts(agentId);
|
||||
|
||||
// 5. Build prompt based on mode
|
||||
String systemPrompt = PromptLoader.loadPrompt("memory/emergence-system");
|
||||
String userPrompt;
|
||||
String userPrompt = buildUserPrompt(mode, topic, oldMemoryContent, scoredCandidates,
|
||||
hasScoredCandidates, dailyNotes);
|
||||
|
||||
if (hasScoredCandidates) {
|
||||
// 使用评分增强的 prompt
|
||||
String candidatesText = formatScoredCandidates(scoredCandidates);
|
||||
String userTemplate = PromptLoader.loadPrompt("memory/emergence-scored-user");
|
||||
userPrompt = userTemplate
|
||||
.replace("{memory}", memoryContent)
|
||||
.replace("{scored_candidates}", candidatesText)
|
||||
.replace("{day_range}", String.valueOf(properties.getEmergenceDayRange()))
|
||||
.replace("{daily_notes}", dailyNotes);
|
||||
log.info("[Memory] Emergence with {} scored candidates for agent={}", scoredCandidates.size(), agentId);
|
||||
} else {
|
||||
// 冷启动:回退到原有纯 LLM 逻辑
|
||||
String userTemplate = PromptLoader.loadPrompt("memory/emergence-user");
|
||||
userPrompt = userTemplate
|
||||
.replace("{memory}", memoryContent)
|
||||
.replace("{day_range}", String.valueOf(properties.getEmergenceDayRange()))
|
||||
.replace("{daily_notes}", dailyNotes);
|
||||
}
|
||||
log.info("[Memory] Emergence {} with {} candidates for agent={}, topic={}",
|
||||
mode, scoredCandidates.size(), agentId, topic);
|
||||
|
||||
// 6. Call LLM
|
||||
String llmResponse;
|
||||
try {
|
||||
ChatModel chatModel = buildChatModel();
|
||||
@ -128,69 +130,146 @@ public class MemoryEmergenceService {
|
||||
llmResponse = response.getResult().getOutput().getText();
|
||||
} catch (Exception e) {
|
||||
log.warn("[Memory] Emergence LLM call failed for agent={}: {}", agentId, e.getMessage());
|
||||
return;
|
||||
return buildFailedReport(agentId, mode, topic, triggerSource, startedAt,
|
||||
scoredCandidates.size(), e.getMessage());
|
||||
}
|
||||
|
||||
// 5. 解析并应用
|
||||
// 7. Parse and apply
|
||||
try {
|
||||
JsonNode root = parseJsonResponse(llmResponse);
|
||||
if (root == null || !root.path("should_update").asBoolean(false)) {
|
||||
String reason = root != null ? root.path("reason").asText("") : "parse failed";
|
||||
log.info("[Memory] No emergence update needed for agent={}: {}", agentId, reason);
|
||||
return;
|
||||
return buildSkippedReport(agentId, mode, topic, triggerSource, startedAt, reason);
|
||||
}
|
||||
|
||||
JsonNode memoryNode = root.path("memory_content");
|
||||
if (!memoryNode.isNull() && memoryNode.isTextual()) {
|
||||
String newContent = memoryNode.asText().trim();
|
||||
if (!newContent.isEmpty()) {
|
||||
workspaceFileService.saveFile(agentId, "MEMORY.md", newContent);
|
||||
String reason = root.path("reason").asText("");
|
||||
log.info("[Memory] Emergence completed for agent={}: {}", agentId, reason);
|
||||
if (memoryNode.isNull() || !memoryNode.isTextual()) {
|
||||
return buildSkippedReport(agentId, mode, topic, triggerSource, startedAt, "no memory_content in response");
|
||||
}
|
||||
|
||||
// 逐候选检查:只有内容被 LLM 实际采纳(出现在新 MEMORY.md 中)的才标记为已提升
|
||||
if (hasScoredCandidates) {
|
||||
List<Long> promotedIds = scoredCandidates.stream()
|
||||
.filter(c -> candidateAdoptedInMemory(c, newContent))
|
||||
.map(MemoryRecallEntity::getId)
|
||||
.collect(Collectors.toList());
|
||||
if (!promotedIds.isEmpty()) {
|
||||
recallService.markPromoted(promotedIds);
|
||||
}
|
||||
log.info("[Memory] Promoted {}/{} recall candidates for agent={}",
|
||||
promotedIds.size(), scoredCandidates.size(), agentId);
|
||||
String newContent = memoryNode.asText().trim();
|
||||
if (newContent.isEmpty()) {
|
||||
return buildSkippedReport(agentId, mode, topic, triggerSource, startedAt, "empty memory_content");
|
||||
}
|
||||
|
||||
// 写入 DREAMS.md 整合日记
|
||||
appendDreamDiary(agentId, scoredCandidates, promotedIds);
|
||||
workspaceFileService.saveFile(agentId, "MEMORY.md", newContent);
|
||||
String llmReason = root.path("reason").asText("");
|
||||
log.info("[Memory] Emergence completed for agent={}: {}", agentId, llmReason);
|
||||
|
||||
// Determine promoted vs rejected candidates
|
||||
List<PromotedEntry> promotedEntries = new ArrayList<>();
|
||||
List<RejectedEntry> rejectedEntries = new ArrayList<>();
|
||||
|
||||
if (hasScoredCandidates) {
|
||||
Set<Long> promotedIds = new HashSet<>();
|
||||
for (MemoryRecallEntity c : scoredCandidates) {
|
||||
if (candidateAdoptedInMemory(c, newContent)) {
|
||||
promotedIds.add(c.getId());
|
||||
promotedEntries.add(new PromotedEntry(
|
||||
c.getId(), c.getFilename(), c.getSnippetPreview(), c.getScore()));
|
||||
} else {
|
||||
// Increment review_count for rejected candidates
|
||||
int newReviewCount = (c.getReviewCount() != null ? c.getReviewCount() : 0) + 1;
|
||||
rejectedEntries.add(new RejectedEntry(
|
||||
c.getId(), c.getFilename(), c.getSnippetPreview(),
|
||||
c.getScore(), newReviewCount));
|
||||
}
|
||||
}
|
||||
|
||||
if (!promotedIds.isEmpty()) {
|
||||
recallService.markPromoted(new ArrayList<>(promotedIds));
|
||||
}
|
||||
// Update review_count / last_reviewed_at for rejected candidates
|
||||
recallService.incrementReviewCounts(
|
||||
rejectedEntries.stream().map(RejectedEntry::recallId).toList());
|
||||
|
||||
log.info("[Memory] Promoted {}/{} recall candidates for agent={}",
|
||||
promotedIds.size(), scoredCandidates.size(), agentId);
|
||||
|
||||
// Append dream diary
|
||||
appendDreamDiary(agentId, scoredCandidates, new ArrayList<>(promotedIds), mode, topic);
|
||||
}
|
||||
|
||||
// Compute diff
|
||||
String memoryDiff = computeDiff(oldMemoryContent, newContent);
|
||||
|
||||
// Build and persist report
|
||||
DreamReport report = buildSuccessReport(agentId, mode, topic, triggerSource, startedAt,
|
||||
scoredCandidates.size(), promotedEntries, rejectedEntries, memoryDiff,
|
||||
truncate(llmReason, 500));
|
||||
persistReport(report);
|
||||
return report;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("[Memory] Failed to parse/apply emergence result for agent={}: {}", agentId, e.getMessage());
|
||||
return buildFailedReport(agentId, mode, topic, triggerSource, startedAt,
|
||||
scoredCandidates.size(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将本轮 dreaming 结果追加到 DREAMS.md 整合日记
|
||||
* Build user prompt based on dream mode.
|
||||
*/
|
||||
private void appendDreamDiary(Long agentId, List<MemoryRecallEntity> allCandidates, List<Long> promotedIds) {
|
||||
private String buildUserPrompt(DreamMode mode, String topic, String memoryContent,
|
||||
List<MemoryRecallEntity> scoredCandidates,
|
||||
boolean hasScoredCandidates, String dailyNotes) {
|
||||
if (mode == DreamMode.FOCUSED && topic != null && !topic.isBlank()) {
|
||||
// FOCUSED mode: use topic-biased prompt
|
||||
String candidatesText = hasScoredCandidates ? formatScoredCandidates(scoredCandidates) : "(no scored candidates)";
|
||||
String userTemplate = PromptLoader.loadPrompt("memory/emergence-focused-user");
|
||||
return userTemplate
|
||||
.replace("{memory}", memoryContent)
|
||||
.replace("{topic}", topic)
|
||||
.replace("{scored_candidates}", candidatesText)
|
||||
.replace("{day_range}", String.valueOf(properties.getEmergenceDayRange()))
|
||||
.replace("{daily_notes}", dailyNotes);
|
||||
}
|
||||
|
||||
// NIGHTLY mode: existing scored or plain prompt
|
||||
if (hasScoredCandidates) {
|
||||
String candidatesText = formatScoredCandidates(scoredCandidates);
|
||||
String userTemplate = PromptLoader.loadPrompt("memory/emergence-scored-user");
|
||||
return userTemplate
|
||||
.replace("{memory}", memoryContent)
|
||||
.replace("{scored_candidates}", candidatesText)
|
||||
.replace("{day_range}", String.valueOf(properties.getEmergenceDayRange()))
|
||||
.replace("{daily_notes}", dailyNotes);
|
||||
} else {
|
||||
String userTemplate = PromptLoader.loadPrompt("memory/emergence-user");
|
||||
return userTemplate
|
||||
.replace("{memory}", memoryContent)
|
||||
.replace("{day_range}", String.valueOf(properties.getEmergenceDayRange()))
|
||||
.replace("{daily_notes}", dailyNotes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append dream diary to DREAMS.md.
|
||||
*/
|
||||
void appendDreamDiary(Long agentId, List<MemoryRecallEntity> allCandidates,
|
||||
List<Long> promotedIds, DreamMode mode, String topic) {
|
||||
try {
|
||||
java.util.Set<Long> promotedSet = new java.util.HashSet<>(promotedIds);
|
||||
Set<Long> promotedSet = new HashSet<>(promotedIds);
|
||||
|
||||
List<MemoryRecallEntity> promoted = allCandidates.stream()
|
||||
.filter(c -> promotedSet.contains(c.getId()))
|
||||
.sorted(java.util.Comparator.comparingDouble(MemoryRecallEntity::getScore).reversed())
|
||||
.sorted(Comparator.comparingDouble(MemoryRecallEntity::getScore).reversed())
|
||||
.toList();
|
||||
List<MemoryRecallEntity> kept = allCandidates.stream()
|
||||
.filter(c -> !promotedSet.contains(c.getId()))
|
||||
.sorted(java.util.Comparator.comparingDouble(MemoryRecallEntity::getScore).reversed())
|
||||
.sorted(Comparator.comparingDouble(MemoryRecallEntity::getScore).reversed())
|
||||
.toList();
|
||||
|
||||
String timestamp = java.time.LocalDateTime.now()
|
||||
String timestamp = LocalDateTime.now()
|
||||
.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));
|
||||
|
||||
StringBuilder diary = new StringBuilder();
|
||||
diary.append("## ").append(timestamp).append(" Dreaming\n\n");
|
||||
diary.append("## ").append(timestamp).append(" Dreaming");
|
||||
if (mode == DreamMode.FOCUSED && topic != null) {
|
||||
diary.append(" [FOCUSED: ").append(topic).append("]");
|
||||
}
|
||||
diary.append("\n\n");
|
||||
diary.append(String.format("**评分候选**: %d 条(阈值 %.1f)\n",
|
||||
allCandidates.size(), properties.getEmergenceScoreThreshold()));
|
||||
diary.append(String.format("**实际整合**: %d 条\n\n", promoted.size()));
|
||||
@ -213,13 +292,13 @@ public class MemoryEmergenceService {
|
||||
diary.append("\n");
|
||||
}
|
||||
|
||||
// 读取现有 DREAMS.md,追加新日记
|
||||
// Read existing DREAMS.md, append new diary
|
||||
String existing = readFileContentSafe(agentId, "DREAMS.md");
|
||||
String newContent = existing.isBlank()
|
||||
? "# Dreaming 整合日记\n\n" + diary
|
||||
: existing + "\n" + diary;
|
||||
|
||||
// 防止无限膨胀:超过 20KB 时截断,只保留最近的内容
|
||||
// Fallback hard truncation at 20KB (Phase 0 behavior preserved when archive flag is off)
|
||||
if (newContent.length() > 20_000) {
|
||||
int cutPoint = newContent.length() - 16_000;
|
||||
int safePoint = newContent.indexOf("\n## ", cutPoint);
|
||||
@ -227,7 +306,6 @@ public class MemoryEmergenceService {
|
||||
newContent = "# Dreaming 整合日记\n\n> 早期记录已归档\n\n"
|
||||
+ newContent.substring(safePoint + 1);
|
||||
} else {
|
||||
// 没找到 ## 标记,硬截断保留最后 16KB
|
||||
newContent = "# Dreaming 整合日记\n\n> 早期记录已归档\n\n"
|
||||
+ newContent.substring(cutPoint);
|
||||
}
|
||||
@ -235,11 +313,80 @@ public class MemoryEmergenceService {
|
||||
|
||||
workspaceFileService.saveFile(agentId, "DREAMS.md", newContent);
|
||||
log.info("[Memory] Dream diary appended for agent={}", agentId);
|
||||
|
||||
// Archive old entries or fall back to 20KB truncation
|
||||
if (properties.getDream().isArchiveEnabled()) {
|
||||
archiveService.archiveOldDreams(agentId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[Memory] Failed to write dream diary for agent={}: {}", agentId, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Report builders ====================
|
||||
|
||||
private DreamReport buildSuccessReport(Long agentId, DreamMode mode, String topic,
|
||||
String triggerSource, LocalDateTime startedAt,
|
||||
int candidateCount,
|
||||
List<PromotedEntry> promoted,
|
||||
List<RejectedEntry> rejected,
|
||||
String memoryDiff, String llmReason) {
|
||||
return new DreamReport(null, agentId, mode, topic, triggerSource, "system",
|
||||
startedAt, LocalDateTime.now(), candidateCount,
|
||||
promoted.size(), rejected.size(), memoryDiff, llmReason,
|
||||
DreamStatus.SUCCESS, null, promoted, rejected);
|
||||
}
|
||||
|
||||
private DreamReport buildSkippedReport(Long agentId, DreamMode mode, String topic,
|
||||
String triggerSource, LocalDateTime startedAt,
|
||||
String reason) {
|
||||
DreamReport report = new DreamReport(null, agentId, mode, topic, triggerSource, "system",
|
||||
startedAt, LocalDateTime.now(), 0, 0, 0, null, reason,
|
||||
DreamStatus.SKIPPED, null, List.of(), List.of());
|
||||
persistReport(report);
|
||||
return report;
|
||||
}
|
||||
|
||||
private DreamReport buildFailedReport(Long agentId, DreamMode mode, String topic,
|
||||
String triggerSource, LocalDateTime startedAt,
|
||||
int candidateCount, String errorMessage) {
|
||||
DreamReport report = new DreamReport(null, agentId, mode, topic, triggerSource, "system",
|
||||
startedAt, LocalDateTime.now(), candidateCount, 0, 0, null, null,
|
||||
DreamStatus.FAILED, errorMessage, List.of(), List.of());
|
||||
persistReport(report);
|
||||
return report;
|
||||
}
|
||||
|
||||
private void persistReport(DreamReport report) {
|
||||
try {
|
||||
DreamReportEntity entity = new DreamReportEntity();
|
||||
entity.setAgentId(report.agentId());
|
||||
entity.setMode(report.mode().name());
|
||||
entity.setTopic(report.topic());
|
||||
entity.setTriggerSource(report.triggerSource());
|
||||
entity.setTriggeredBy(report.triggeredBy());
|
||||
entity.setStartedAt(report.startedAt());
|
||||
entity.setFinishedAt(report.finishedAt());
|
||||
entity.setCandidateCount(report.candidateCount());
|
||||
entity.setPromotedCount(report.promotedCount());
|
||||
entity.setRejectedCount(report.rejectedCount());
|
||||
entity.setMemoryDiff(report.memoryDiff());
|
||||
entity.setLlmReason(report.llmReason());
|
||||
entity.setStatus(report.status().name());
|
||||
entity.setErrorMessage(report.errorMessage());
|
||||
entity.setCreateTime(LocalDateTime.now());
|
||||
entity.setUpdateTime(LocalDateTime.now());
|
||||
entity.setDeleted(0);
|
||||
dreamReportMapper.insert(entity);
|
||||
log.debug("[Memory] DreamReport persisted: agent={}, mode={}, status={}",
|
||||
report.agentId(), report.mode(), report.status());
|
||||
} catch (Exception e) {
|
||||
log.warn("[Memory] Failed to persist DreamReport for agent={}: {}", report.agentId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Helpers ====================
|
||||
|
||||
private String formatScoredCandidates(List<MemoryRecallEntity> candidates) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (MemoryRecallEntity entry : candidates) {
|
||||
@ -253,16 +400,11 @@ public class MemoryEmergenceService {
|
||||
return sb.toString().trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断候选片段是否被 LLM 实际采纳到新 MEMORY.md 中。
|
||||
* 通过检查片段预览中的关键短语(取前 3 个非空行的前 20 字符)是否出现在新内容中。
|
||||
*/
|
||||
private boolean candidateAdoptedInMemory(MemoryRecallEntity candidate, String newMemoryContent) {
|
||||
String preview = candidate.getSnippetPreview();
|
||||
if (preview == null || preview.isBlank() || newMemoryContent == null) {
|
||||
return false;
|
||||
}
|
||||
// 从 snippet 提取关键短语进行匹配
|
||||
String[] lines = preview.split("\n");
|
||||
int matched = 0;
|
||||
int checked = 0;
|
||||
@ -271,17 +413,29 @@ public class MemoryEmergenceService {
|
||||
if (trimmed.isEmpty() || trimmed.startsWith("#")) continue;
|
||||
if (checked >= 3) break;
|
||||
checked++;
|
||||
// 取行的核心内容(去掉 markdown 标记),检查是否出现在新 MEMORY.md 中
|
||||
String key = trimmed.replaceAll("^[-*>]+\\s*", "");
|
||||
if (key.length() > 20) key = key.substring(0, 20);
|
||||
if (key.length() >= 5 && newMemoryContent.contains(key)) {
|
||||
matched++;
|
||||
}
|
||||
}
|
||||
// 至少有 1 个关键短语命中才算采纳
|
||||
return matched > 0;
|
||||
}
|
||||
|
||||
private String computeDiff(String oldContent, String newContent) {
|
||||
if (oldContent == null || oldContent.isBlank()) return "(new file)";
|
||||
if (oldContent.equals(newContent)) return "(no change)";
|
||||
// Simple line-count diff for Phase 1
|
||||
int oldLines = oldContent.split("\n").length;
|
||||
int newLines = newContent.split("\n").length;
|
||||
return String.format("-%d/+%d lines", oldLines, newLines);
|
||||
}
|
||||
|
||||
private String truncate(String s, int maxLen) {
|
||||
if (s == null) return null;
|
||||
return s.length() <= maxLen ? s : s.substring(0, maxLen) + "...";
|
||||
}
|
||||
|
||||
private ChatModel buildChatModel() {
|
||||
ModelConfigEntity defaultModel = modelConfigService.getDefaultModel();
|
||||
return agentGraphBuilder.buildRuntimeChatModel(defaultModel);
|
||||
@ -309,7 +463,7 @@ public class MemoryEmergenceService {
|
||||
}
|
||||
}
|
||||
|
||||
private String readFileContentSafe(Long agentId, String filename) {
|
||||
String readFileContentSafe(Long agentId, String filename) {
|
||||
try {
|
||||
WorkspaceFileEntity file = workspaceFileService.getFile(agentId, filename);
|
||||
return file != null && file.getContent() != null ? file.getContent() : "";
|
||||
|
||||
@ -227,7 +227,7 @@ public class MemoryRecallService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记候选为已提升
|
||||
* Mark candidates as promoted to MEMORY.md.
|
||||
*/
|
||||
public void markPromoted(List<Long> ids) {
|
||||
if (ids == null || ids.isEmpty()) return;
|
||||
@ -237,6 +237,21 @@ public class MemoryRecallService {
|
||||
.set(MemoryRecallEntity::getPromoted, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment review_count and set last_reviewed_at for rejected candidates.
|
||||
* Phase 1: write-only; filtering by review_count is deferred to Phase 2.
|
||||
*/
|
||||
public void incrementReviewCounts(List<Long> ids) {
|
||||
if (ids == null || ids.isEmpty()) return;
|
||||
for (Long id : ids) {
|
||||
recallMapper.update(null,
|
||||
new LambdaUpdateWrapper<MemoryRecallEntity>()
|
||||
.eq(MemoryRecallEntity::getId, id)
|
||||
.setSql("review_count = COALESCE(review_count, 0) + 1")
|
||||
.set(MemoryRecallEntity::getLastReviewedAt, java.time.LocalDateTime.now()));
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 查询方法(供 API 使用) ====================
|
||||
|
||||
/**
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
package vip.mate.memory.service;
|
||||
|
||||
/**
|
||||
* A candidate that was adopted into MEMORY.md during a dream.
|
||||
*
|
||||
* @author MateClaw Team
|
||||
*/
|
||||
public record PromotedEntry(
|
||||
Long recallId,
|
||||
String filename,
|
||||
String snippetPreview,
|
||||
double score
|
||||
) {}
|
||||
@ -0,0 +1,14 @@
|
||||
package vip.mate.memory.service;
|
||||
|
||||
/**
|
||||
* A candidate that was scored but not adopted into MEMORY.md during a dream.
|
||||
*
|
||||
* @author MateClaw Team
|
||||
*/
|
||||
public record RejectedEntry(
|
||||
Long recallId,
|
||||
String filename,
|
||||
String snippetPreview,
|
||||
double score,
|
||||
int reviewCount
|
||||
) {}
|
||||
@ -0,0 +1,24 @@
|
||||
-- Dream v2: structured dream report (rfc-035 §4.4)
|
||||
CREATE TABLE IF NOT EXISTS mate_dream_report (
|
||||
id BIGINT PRIMARY KEY,
|
||||
agent_id BIGINT NOT NULL,
|
||||
mode VARCHAR(32) NOT NULL,
|
||||
topic VARCHAR(256),
|
||||
trigger_source VARCHAR(32) NOT NULL,
|
||||
triggered_by VARCHAR(64),
|
||||
started_at DATETIME NOT NULL,
|
||||
finished_at DATETIME NOT NULL,
|
||||
candidate_count INT NOT NULL,
|
||||
promoted_count INT NOT NULL,
|
||||
rejected_count INT NOT NULL,
|
||||
memory_diff TEXT,
|
||||
llm_reason TEXT,
|
||||
status VARCHAR(16) NOT NULL,
|
||||
error_message TEXT,
|
||||
create_time DATETIME NOT NULL,
|
||||
update_time DATETIME NOT NULL,
|
||||
deleted TINYINT DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_dream_agent_time ON mate_dream_report(agent_id, started_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_dream_agent_mode ON mate_dream_report(agent_id, mode, started_at DESC);
|
||||
@ -0,0 +1,4 @@
|
||||
-- Dream v2: candidate state machine fields (rfc-035 §4.1.4)
|
||||
-- Phase 1 writes values only; filtering enabled in Phase 2.
|
||||
ALTER TABLE mate_memory_recall ADD COLUMN IF NOT EXISTS review_count INT DEFAULT 0;
|
||||
ALTER TABLE mate_memory_recall ADD COLUMN IF NOT EXISTS last_reviewed_at DATETIME;
|
||||
@ -0,0 +1,24 @@
|
||||
-- Dream v2: structured dream report (rfc-035 §4.4)
|
||||
CREATE TABLE IF NOT EXISTS mate_dream_report (
|
||||
id BIGINT PRIMARY KEY,
|
||||
agent_id BIGINT NOT NULL,
|
||||
mode VARCHAR(32) NOT NULL,
|
||||
topic VARCHAR(256),
|
||||
trigger_source VARCHAR(32) NOT NULL,
|
||||
triggered_by VARCHAR(64),
|
||||
started_at DATETIME NOT NULL,
|
||||
finished_at DATETIME NOT NULL,
|
||||
candidate_count INT NOT NULL,
|
||||
promoted_count INT NOT NULL,
|
||||
rejected_count INT NOT NULL,
|
||||
memory_diff TEXT,
|
||||
llm_reason TEXT,
|
||||
status VARCHAR(16) NOT NULL,
|
||||
error_message TEXT,
|
||||
create_time DATETIME NOT NULL,
|
||||
update_time DATETIME NOT NULL,
|
||||
deleted TINYINT DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE INDEX idx_dream_agent_time ON mate_dream_report(agent_id, started_at DESC);
|
||||
CREATE INDEX idx_dream_agent_mode ON mate_dream_report(agent_id, mode, started_at DESC);
|
||||
@ -0,0 +1,18 @@
|
||||
-- Dream v2: candidate state machine fields (rfc-035 §4.1.4)
|
||||
-- Phase 1 writes values only; filtering enabled in Phase 2.
|
||||
-- MySQL does not support ADD COLUMN IF NOT EXISTS; use INFORMATION_SCHEMA guard.
|
||||
SET @db_name = DATABASE();
|
||||
|
||||
SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db_name AND TABLE_NAME = 'mate_memory_recall' AND COLUMN_NAME = 'review_count');
|
||||
SET @stmt = IF(@col_exists = 0,
|
||||
'ALTER TABLE mate_memory_recall ADD COLUMN review_count INT DEFAULT 0',
|
||||
'SELECT 1');
|
||||
PREPARE s FROM @stmt; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
|
||||
SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db_name AND TABLE_NAME = 'mate_memory_recall' AND COLUMN_NAME = 'last_reviewed_at');
|
||||
SET @stmt = IF(@col_exists = 0,
|
||||
'ALTER TABLE mate_memory_recall ADD COLUMN last_reviewed_at DATETIME',
|
||||
'SELECT 1');
|
||||
PREPARE s FROM @stmt; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
@ -0,0 +1,23 @@
|
||||
## 现有 MEMORY.md 内容
|
||||
```
|
||||
{memory}
|
||||
```
|
||||
|
||||
## Focused Dream 主题
|
||||
|
||||
本次整合的重点主题:**{topic}**
|
||||
|
||||
请优先提炼与该主题直接相关的记忆(约 60% 权重),同时也关注非主题类的高分候选(约 40% 权重),避免因主题聚焦而遗漏重要信息。
|
||||
|
||||
## 高分召回候选(按评分排序)
|
||||
|
||||
{scored_candidates}
|
||||
|
||||
## 最近 {day_range} 天的 daily notes(补充参考)
|
||||
|
||||
{daily_notes}
|
||||
|
||||
---
|
||||
|
||||
请围绕主题 "{topic}" 分析以上内容,将相关的稳定信息整合到 MEMORY.md 中。
|
||||
非主题类的高分候选如有重要内容也一并整合。严格输出 JSON 格式。
|
||||
Loading…
Reference in New Issue
Block a user