diff --git a/mateclaw-server/src/main/java/vip/mate/agent/AgentService.java b/mateclaw-server/src/main/java/vip/mate/agent/AgentService.java index 3b4c886f..b7a95cbd 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/AgentService.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/AgentService.java @@ -215,6 +215,20 @@ public class AgentService { agentInstances.remove(agentId); } + /** + * Invalidate the cached agent instance whenever one of its workspace files + * changes. The system prompt (which embeds MEMORY.md / PROFILE.md / structured + * memory) is baked into the cached instance at build time, so memory edits made + * via tools, consolidation, or cleanup would otherwise stay invisible until an + * agent config change or restart. Rebuilding on the next turn picks them up. + */ + @org.springframework.context.event.EventListener + public void onWorkspaceFileChanged(vip.mate.workspace.document.event.WorkspaceFileChangedEvent event) { + if (event.agentId() != null) { + agentInstances.remove(event.agentId()); + } + } + // ==================== 运行时入口 ==================== public String chat(Long agentId, String message, String conversationId) { diff --git a/mateclaw-server/src/main/java/vip/mate/workspace/document/WorkspaceFileService.java b/mateclaw-server/src/main/java/vip/mate/workspace/document/WorkspaceFileService.java index 58b6c4fa..5f900f9d 100644 --- a/mateclaw-server/src/main/java/vip/mate/workspace/document/WorkspaceFileService.java +++ b/mateclaw-server/src/main/java/vip/mate/workspace/document/WorkspaceFileService.java @@ -3,8 +3,10 @@ package vip.mate.workspace.document; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import vip.mate.workspace.document.event.WorkspaceFileChangedEvent; import vip.mate.workspace.document.model.WorkspaceFileEntity; import vip.mate.workspace.document.repository.WorkspaceFileMapper; @@ -31,6 +33,7 @@ import java.util.stream.Collectors; public class WorkspaceFileService { private final WorkspaceFileMapper fileMapper; + private final ApplicationEventPublisher eventPublisher; /** * 列出 Agent 的所有工作区文件(按排序 + 文件名排列) @@ -68,6 +71,7 @@ public class WorkspaceFileService { existing.setContent(content); existing.setFileSize(size); fileMapper.updateById(existing); + eventPublisher.publishEvent(new WorkspaceFileChangedEvent(agentId, filename)); return existing; } else { WorkspaceFileEntity entity = new WorkspaceFileEntity(); @@ -78,6 +82,7 @@ public class WorkspaceFileService { entity.setEnabled(false); entity.setSortOrder(0); fileMapper.insert(entity); + eventPublisher.publishEvent(new WorkspaceFileChangedEvent(agentId, filename)); return entity; } } @@ -91,6 +96,7 @@ public class WorkspaceFileService { new LambdaQueryWrapper() .eq(WorkspaceFileEntity::getAgentId, agentId) .eq(WorkspaceFileEntity::getFilename, filename)); + eventPublisher.publishEvent(new WorkspaceFileChangedEvent(agentId, filename)); } /** diff --git a/mateclaw-server/src/main/java/vip/mate/workspace/document/event/WorkspaceFileChangedEvent.java b/mateclaw-server/src/main/java/vip/mate/workspace/document/event/WorkspaceFileChangedEvent.java new file mode 100644 index 00000000..ddaac410 --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/workspace/document/event/WorkspaceFileChangedEvent.java @@ -0,0 +1,16 @@ +package vip.mate.workspace.document.event; + +/** + * Published whenever an agent's workspace file is created, updated, or deleted. + *

+ * Workspace files (AGENTS.md, SOUL.md, PROFILE.md, MEMORY.md, structured/*.md) + * are baked into the agent's system prompt when its runtime instance is built. + * Listeners use this to invalidate the cached agent instance so memory edits + * (tool writes, consolidation, cleanup) take effect on the next turn instead of + * only after an agent config change or restart. + * + * @param agentId the affected agent + * @param filename the workspace file that changed + */ +public record WorkspaceFileChangedEvent(Long agentId, String filename) { +} diff --git a/mateclaw-server/src/test/java/vip/mate/workspace/document/WorkspaceMemorySearchTest.java b/mateclaw-server/src/test/java/vip/mate/workspace/document/WorkspaceMemorySearchTest.java index 8989778c..bc49eb4c 100644 --- a/mateclaw-server/src/test/java/vip/mate/workspace/document/WorkspaceMemorySearchTest.java +++ b/mateclaw-server/src/test/java/vip/mate/workspace/document/WorkspaceMemorySearchTest.java @@ -38,6 +38,7 @@ import static org.mockito.Mockito.when; class WorkspaceMemorySearchTest { @Mock private WorkspaceFileMapper fileMapper; + @Mock private org.springframework.context.ApplicationEventPublisher eventPublisher; private WorkspaceFileService service; @BeforeAll @@ -51,7 +52,21 @@ class WorkspaceMemorySearchTest { @BeforeEach void setUp() { - service = new WorkspaceFileService(fileMapper); + service = new WorkspaceFileService(fileMapper, eventPublisher); + } + + @Test + @DisplayName("saveFile publishes a change event so the cached agent instance is invalidated") + void saveFilePublishesChangeEvent() { + when(fileMapper.selectOne(any())).thenReturn(null); // new file path + + service.saveFile(1000000001L, "MEMORY.md", "## 稳定事实\n- 用户语言:简体中文"); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(vip.mate.workspace.document.event.WorkspaceFileChangedEvent.class); + org.mockito.Mockito.verify(eventPublisher).publishEvent(captor.capture()); + assertThat(captor.getValue().agentId()).isEqualTo(1000000001L); + assertThat(captor.getValue().filename()).isEqualTo("MEMORY.md"); } // ---------- tokenize ----------