mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
fix(context): keep the latest summary even when its row sits outside the recent window; include summaryId in compact_status
This commit is contained in:
parent
faf7f98358
commit
af56763156
@ -241,16 +241,40 @@ public abstract class BaseAgent {
|
||||
// boundary's content is already folded into the newer summary. Walking
|
||||
// forward and breaking on the first boundary kept everything between
|
||||
// boundaries — the very redundancy compaction was supposed to remove.
|
||||
boolean boundaryFoundInWindow = false;
|
||||
for (int i = history.size() - 1; i >= 0; i--) {
|
||||
MessageEntity msg = history.get(i);
|
||||
if ("system".equals(msg.getRole()) && isCompressionSummary(msg)) {
|
||||
history = new ArrayList<>(history.subList(i, history.size()));
|
||||
boundaryFoundInWindow = true;
|
||||
log.info("[{}] Found latest compression boundary at index {}; loading {} messages forward",
|
||||
agentName, i, history.size());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Latest boundary may live OUTSIDE the recent window =====
|
||||
// On a long conversation that compacted hours/days ago and has paged
|
||||
// fewer than `windowSize` new messages since, `listRecentMessages`
|
||||
// returns only the raw tail — the boundary sat at index 0 of the
|
||||
// original list and never made it into `history`. Without prepending
|
||||
// it, the model would forget the original goal even though we already
|
||||
// paid the LLM cost to produce a structured summary.
|
||||
if (!boundaryFoundInWindow && totalCount > windowSize) {
|
||||
try {
|
||||
MessageEntity latestBoundary = conversationService.findLatestCompressionBoundary(conversationId);
|
||||
if (latestBoundary != null) {
|
||||
history = new ArrayList<>(history);
|
||||
history.add(0, latestBoundary);
|
||||
log.info("[{}] Prepended out-of-window compression boundary id={} so the model keeps the summary context",
|
||||
agentName, latestBoundary.getId());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[{}] findLatestCompressionBoundary failed; loading recent window without boundary: {}",
|
||||
agentName, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 转换为 Spring AI Message 对象 =====
|
||||
int limit = history.size();
|
||||
if (limit > 0) {
|
||||
|
||||
@ -464,13 +464,19 @@ public class ConversationWindowManager {
|
||||
boundaryMetadata.put("tailKept", recentMessages.size());
|
||||
boundaryMetadata.put("toolResultsSpilled", spillsThisTurn);
|
||||
boundaryMetadata.put("anchored", anchored);
|
||||
Long summaryId = null;
|
||||
try {
|
||||
conversationService.saveCompressionSummary(
|
||||
summaryId = conversationService.saveCompressionSummaryReturningId(
|
||||
conversationId, SUMMARY_PREFIX + summary, oldMessages.size(),
|
||||
boundaryMetadata);
|
||||
} catch (Exception e) {
|
||||
log.warn("[ConversationWindow] Failed to persist compression boundary: {}", e.getMessage());
|
||||
}
|
||||
if (summaryId != null) {
|
||||
// Mirror the DB row's metadata: the SSE consumer needs the id
|
||||
// to deep-link the boundary card without having to refetch.
|
||||
boundaryMetadata.put("summaryId", summaryId);
|
||||
}
|
||||
broadcastCompactStatus(conversationId, "done", boundaryMetadata);
|
||||
} else if (summary != null && !summary.isBlank() && fromCache) {
|
||||
// Cached summary path — no new DB row, but emit done so the
|
||||
|
||||
@ -387,6 +387,30 @@ public class ConversationService {
|
||||
.orderByAsc(MessageEntity::getId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the most recent compression boundary row for the conversation,
|
||||
* or {@code null} if no boundary exists yet. Used by the agent loader to
|
||||
* recover the structured summary when the boundary itself sits outside the
|
||||
* recent-message window — without this, a long conversation that already
|
||||
* compacted would feed the model the last N raw messages while silently
|
||||
* dropping the goal / progress digest the boundary holds.
|
||||
*
|
||||
* <p>Implemented as a single indexed query rather than a full
|
||||
* {@code listMessages} + filter so it stays cheap on conversations with
|
||||
* thousands of messages. Selection: {@code role=system} +
|
||||
* {@code metadata like '%compression_summary%'} (the metadata column always
|
||||
* carries that literal — see {@link #saveCompressionSummary}).
|
||||
*/
|
||||
public MessageEntity findLatestCompressionBoundary(String conversationId) {
|
||||
return messageMapper.selectOne(new LambdaQueryWrapper<MessageEntity>()
|
||||
.eq(MessageEntity::getConversationId, conversationId)
|
||||
.eq(MessageEntity::getRole, "system")
|
||||
.like(MessageEntity::getMetadata, "compression_summary")
|
||||
.orderByDesc(MessageEntity::getCreateTime)
|
||||
.orderByDesc(MessageEntity::getId)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载最近 N 条消息(倒序取出后翻转为正序)。
|
||||
* 利用复合索引 (conversation_id, create_time) 高效分页。
|
||||
@ -442,6 +466,24 @@ public class ConversationService {
|
||||
saveCompressionSummary(conversationId, summary, compressedCount, Map.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as {@link #saveCompressionSummary(String, String, int, Map)} but
|
||||
* returns the inserted row's id so callers (notably
|
||||
* {@code ConversationWindowManager}) can include the {@code summaryId}
|
||||
* in the {@code compact_status} SSE payload. The id is also written back
|
||||
* into the row's metadata JSON by the underlying overload, so the row is
|
||||
* still self-describing if a client misses the SSE event and loads
|
||||
* history later.
|
||||
*
|
||||
* <p>Returns {@code null} when the insert path failed (logged at INFO);
|
||||
* callers should treat that as "no boundary was persisted" and still
|
||||
* broadcast a {@code done} event without {@code summaryId}.
|
||||
*/
|
||||
public Long saveCompressionSummaryReturningId(String conversationId, String summary,
|
||||
int compressedCount, Map<String, Object> extraMetadata) {
|
||||
return saveCompressionSummaryInternal(conversationId, summary, compressedCount, extraMetadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as the 3-arg overload but accepts extra structured fields that
|
||||
* are merged into the boundary's metadata JSON. Fields the frontend
|
||||
@ -463,6 +505,11 @@ public class ConversationService {
|
||||
*/
|
||||
public void saveCompressionSummary(String conversationId, String summary, int compressedCount,
|
||||
Map<String, Object> extraMetadata) {
|
||||
saveCompressionSummaryInternal(conversationId, summary, compressedCount, extraMetadata);
|
||||
}
|
||||
|
||||
private Long saveCompressionSummaryInternal(String conversationId, String summary, int compressedCount,
|
||||
Map<String, Object> extraMetadata) {
|
||||
MessageEntity entity = new MessageEntity();
|
||||
entity.setConversationId(conversationId);
|
||||
entity.setRole("system");
|
||||
@ -506,6 +553,7 @@ public class ConversationService {
|
||||
}
|
||||
log.info("[Conversation] Saved compression boundary conv={}, compressedCount={}, metadata={}",
|
||||
conversationId, compressedCount, entity.getMetadata());
|
||||
return entity.getId();
|
||||
}
|
||||
|
||||
public List<MessageVO> listMessageViews(String conversationId) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user