[](https://github.com/matevip/mateclaw)
[](https://claw.mate.vip/docs)
diff --git a/mateclaw-server/src/main/java/vip/mate/memory/controller/DreamController.java b/mateclaw-server/src/main/java/vip/mate/memory/controller/DreamController.java
index aa4aa4f7..d55cbd86 100644
--- a/mateclaw-server/src/main/java/vip/mate/memory/controller/DreamController.java
+++ b/mateclaw-server/src/main/java/vip/mate/memory/controller/DreamController.java
@@ -139,14 +139,13 @@ public class DreamController {
private Long resolveUserId(Authentication auth) {
if (auth == null) return null;
- // Resolve from auth principal — assumes user ID is accessible
try {
Object principal = auth.getPrincipal();
if (principal instanceof vip.mate.auth.model.UserEntity user) {
return user.getId();
}
- // Fallback: use username hash as stable ID
- return (long) auth.getName().hashCode();
+ // Fallback: use abs(hashCode) to avoid negative IDs, add offset to avoid collision with real IDs
+ return Math.abs((long) auth.getName().hashCode()) + 1_000_000_000L;
} catch (Exception e) {
return null;
}
diff --git a/mateclaw-server/src/main/java/vip/mate/memory/fact/projection/FactProjectionBuilder.java b/mateclaw-server/src/main/java/vip/mate/memory/fact/projection/FactProjectionBuilder.java
index 1ab6667f..630bd90a 100644
--- a/mateclaw-server/src/main/java/vip/mate/memory/fact/projection/FactProjectionBuilder.java
+++ b/mateclaw-server/src/main/java/vip/mate/memory/fact/projection/FactProjectionBuilder.java
@@ -1,11 +1,13 @@
package vip.mate.memory.fact.projection;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import vip.mate.memory.MemoryProperties;
import vip.mate.memory.fact.extraction.CompositeEntityExtractor;
import vip.mate.memory.fact.extraction.ExtractedFact;
+import vip.mate.memory.fact.model.FactEntity;
import vip.mate.memory.fact.repository.FactMapper;
import vip.mate.workspace.document.WorkspaceFileService;
import vip.mate.workspace.document.model.WorkspaceFileEntity;
@@ -18,9 +20,10 @@ import java.util.List;
* Rebuilds the fact projection from canonical sources.
*
* Derived columns are overwritten; accumulated columns (use_count, last_used_at)
- * are preserved via MERGE/upsert keyed on (agent_id, source_ref).
+ * are preserved via select-then-update keyed on (agent_id, source_ref).
*
* Only this class may write derived columns to mate_fact (core invariant).
+ * Uses MyBatis Plus CRUD (dialect-safe for both H2 and MySQL).
*
* @author MateClaw Team
*/
@@ -65,13 +68,11 @@ public class FactProjectionBuilder {
allFacts.addAll(extractor.extract(agentId, "MEMORY.md", memoryFile.getContent()));
}
- // Upsert all extracted facts
+ // Upsert all extracted facts (dialect-safe)
LocalDateTime now = LocalDateTime.now();
List keepRefs = new ArrayList<>();
for (ExtractedFact fact : allFacts) {
- factMapper.upsertDerivedH2(agentId, fact.sourceRef(), fact.category(),
- fact.subject(), fact.predicate(), fact.objectValue(),
- fact.confidence(), 0.5, fact.extractedBy(), now, now);
+ upsertDerived(agentId, fact, now);
keepRefs.add(fact.sourceRef());
}
@@ -93,11 +94,50 @@ public class FactProjectionBuilder {
List facts = extractor.extract(agentId, filename, content);
LocalDateTime now = LocalDateTime.now();
for (ExtractedFact fact : facts) {
- factMapper.upsertDerivedH2(agentId, fact.sourceRef(), fact.category(),
- fact.subject(), fact.predicate(), fact.objectValue(),
- fact.confidence(), 0.5, fact.extractedBy(), now, now);
+ upsertDerived(agentId, fact, now);
}
log.debug("[FactProjection] rebuildOne: agent={}, file={}, facts={}", agentId, filename, facts.size());
return facts.size();
}
+
+ /**
+ * Dialect-safe upsert: select by (agent_id, source_ref), then insert or update.
+ * Preserves accumulated columns (use_count, last_used_at) on update.
+ */
+ private void upsertDerived(Long agentId, ExtractedFact fact, LocalDateTime now) {
+ FactEntity existing = factMapper.selectOne(
+ new LambdaQueryWrapper()
+ .eq(FactEntity::getAgentId, agentId)
+ .eq(FactEntity::getSourceRef, fact.sourceRef())
+ .last("LIMIT 1"));
+
+ if (existing != null) {
+ // Update derived columns only; preserve accumulated columns
+ existing.setCategory(fact.category());
+ existing.setSubject(fact.subject());
+ existing.setPredicate(fact.predicate());
+ existing.setObjectValue(fact.objectValue());
+ existing.setConfidence(fact.confidence());
+ existing.setExtractedBy(fact.extractedBy());
+ existing.setUpdateTime(now);
+ existing.setDeleted(0); // un-delete if previously soft-deleted
+ factMapper.updateById(existing);
+ } else {
+ FactEntity entity = new FactEntity();
+ entity.setAgentId(agentId);
+ entity.setSourceRef(fact.sourceRef());
+ entity.setCategory(fact.category());
+ entity.setSubject(fact.subject());
+ entity.setPredicate(fact.predicate());
+ entity.setObjectValue(fact.objectValue());
+ entity.setConfidence(fact.confidence());
+ entity.setTrust(0.5);
+ entity.setUseCount(0);
+ entity.setExtractedBy(fact.extractedBy());
+ entity.setCreateTime(now);
+ entity.setUpdateTime(now);
+ entity.setDeleted(0);
+ factMapper.insert(entity);
+ }
+ }
}
diff --git a/mateclaw-server/src/main/java/vip/mate/memory/fact/repository/FactMapper.java b/mateclaw-server/src/main/java/vip/mate/memory/fact/repository/FactMapper.java
index ff257925..7a98302c 100644
--- a/mateclaw-server/src/main/java/vip/mate/memory/fact/repository/FactMapper.java
+++ b/mateclaw-server/src/main/java/vip/mate/memory/fact/repository/FactMapper.java
@@ -11,7 +11,7 @@ import java.util.List;
/**
* Fact mapper — limited write operations enforce the core invariant:
- * - Derived columns: only FactProjectionBuilder may write
+ * - Derived columns: only FactProjectionBuilder may write (via upsertDerived)
* - Accumulated columns: only bumpUseCount may write
*
* @author MateClaw Team
@@ -19,29 +19,6 @@ import java.util.List;
@Mapper
public interface FactMapper extends BaseMapper {
- /**
- * Upsert derived columns by (agent_id, source_ref).
- * Preserves accumulated columns (last_used_at, use_count).
- */
- @Update("""
- MERGE INTO mate_fact (agent_id, source_ref, category, subject, predicate, object_value,
- confidence, trust, extracted_by, create_time, update_time, deleted)
- KEY (agent_id, source_ref)
- VALUES (#{agentId}, #{sourceRef}, #{category}, #{subject}, #{predicate}, #{objectValue},
- #{confidence}, #{trust}, #{extractedBy}, #{createTime}, #{updateTime}, 0)
- """)
- void upsertDerivedH2(@Param("agentId") Long agentId,
- @Param("sourceRef") String sourceRef,
- @Param("category") String category,
- @Param("subject") String subject,
- @Param("predicate") String predicate,
- @Param("objectValue") String objectValue,
- @Param("confidence") Double confidence,
- @Param("trust") Double trust,
- @Param("extractedBy") String extractedBy,
- @Param("createTime") LocalDateTime createTime,
- @Param("updateTime") LocalDateTime updateTime);
-
/**
* Bump use_count and last_used_at for the given fact IDs.
* This is the ONLY path that writes accumulated columns.
diff --git a/mateclaw-server/src/main/java/vip/mate/memory/service/MorningCardService.java b/mateclaw-server/src/main/java/vip/mate/memory/service/MorningCardService.java
index f98ad412..1037d4ae 100644
--- a/mateclaw-server/src/main/java/vip/mate/memory/service/MorningCardService.java
+++ b/mateclaw-server/src/main/java/vip/mate/memory/service/MorningCardService.java
@@ -31,6 +31,7 @@ public class MorningCardService {
* Get the morning card for a user+agent. Returns null if no unseen dream exists.
*/
public Map getCardFor(Long userId, Long agentId) {
+ if (userId == null || agentId == null) return null;
// Find the latest successful dream report for this agent
DreamReportEntity latestReport = dreamReportMapper.selectOne(
new LambdaQueryWrapper()
diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts
index 2da4fe27..09727652 100644
--- a/mateclaw-ui/src/i18n/locales/en-US.ts
+++ b/mateclaw-ui/src/i18n/locales/en-US.ts
@@ -1795,6 +1795,7 @@ export default {
modeNightly: 'Nightly',
candidates: 'candidates',
loadMore: 'Load more',
+ retry: 'Retry',
time: {
justNow: 'just now',
hoursAgo: '{n}h ago',
diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts
index 1ca9dd2b..3928defd 100644
--- a/mateclaw-ui/src/i18n/locales/zh-CN.ts
+++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts
@@ -1805,6 +1805,7 @@ export default {
modeNightly: '夜间',
candidates: '条候选',
loadMore: '加载更多',
+ retry: '重试',
time: {
justNow: '刚刚',
hoursAgo: '{n}小时前',
diff --git a/mateclaw-ui/src/stores/useMemoryStore.ts b/mateclaw-ui/src/stores/useMemoryStore.ts
index 203a4b31..4080a1b4 100644
--- a/mateclaw-ui/src/stores/useMemoryStore.ts
+++ b/mateclaw-ui/src/stores/useMemoryStore.ts
@@ -24,17 +24,23 @@ export const useMemoryStore = defineStore('memory', () => {
const reports = ref([])
const total = ref(0)
const loading = ref(false)
+ const error = ref(null)
const currentReport = ref(null)
- let eventSource: EventSource | null = null
+ let pollTimer: ReturnType | null = null
async function fetchReports(agentId: number, page = 1, size = 20) {
loading.value = true
+ error.value = null
try {
const res = await http.get(`/memory/${agentId}/dream/reports`, {
params: { page, size },
})
reports.value = res.data.records || []
total.value = res.data.total || 0
+ } catch (e: any) {
+ error.value = e.message || 'Failed to load reports'
+ reports.value = []
+ total.value = 0
} finally {
loading.value = false
}
@@ -45,46 +51,34 @@ export const useMemoryStore = defineStore('memory', () => {
try {
const res = await http.get(`/memory/${agentId}/dream/reports/${reportId}`)
currentReport.value = res.data
+ } catch {
+ currentReport.value = null
} finally {
loading.value = false
}
}
/**
- * Subscribe to dream SSE events for an agent.
- * Automatically refreshes the report list on new dream events.
+ * Poll for new dream events instead of SSE.
+ * SSE via EventSource doesn't support Authorization headers,
+ * causing 401 errors. Polling every 15s is sufficient for dream events.
*/
function subscribeEvents(agentId: number) {
unsubscribeEvents()
- const token = localStorage.getItem('token')
- const url = `/api/v1/memory/${agentId}/dream/events`
- eventSource = new EventSource(url)
-
- eventSource.addEventListener('dream.completed', (e) => {
- // Refresh the report list to show the new dream
+ pollTimer = setInterval(() => {
fetchReports(agentId, 1, 20)
- })
-
- eventSource.addEventListener('dream.failed', (e) => {
- fetchReports(agentId, 1, 20)
- })
-
- eventSource.onerror = () => {
- // Reconnect after 5s on error
- unsubscribeEvents()
- setTimeout(() => subscribeEvents(agentId), 5000)
- }
+ }, 15000)
}
function unsubscribeEvents() {
- if (eventSource) {
- eventSource.close()
- eventSource = null
+ if (pollTimer) {
+ clearInterval(pollTimer)
+ pollTimer = null
}
}
return {
- reports, total, loading, currentReport,
+ reports, total, loading, error, currentReport,
fetchReports, fetchReport,
subscribeEvents, unsubscribeEvents,
}
diff --git a/mateclaw-ui/src/views/Memory/index.vue b/mateclaw-ui/src/views/Memory/index.vue
index 02cde980..0a035e21 100644
--- a/mateclaw-ui/src/views/Memory/index.vue
+++ b/mateclaw-ui/src/views/Memory/index.vue
@@ -63,6 +63,13 @@