mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-16 04:18:17 +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
|
// boundary's content is already folded into the newer summary. Walking
|
||||||
// forward and breaking on the first boundary kept everything between
|
// forward and breaking on the first boundary kept everything between
|
||||||
// boundaries — the very redundancy compaction was supposed to remove.
|
// boundaries — the very redundancy compaction was supposed to remove.
|
||||||
|
boolean boundaryFoundInWindow = false;
|
||||||
for (int i = history.size() - 1; i >= 0; i--) {
|
for (int i = history.size() - 1; i >= 0; i--) {
|
||||||
MessageEntity msg = history.get(i);
|
MessageEntity msg = history.get(i);
|
||||||
if ("system".equals(msg.getRole()) && isCompressionSummary(msg)) {
|
if ("system".equals(msg.getRole()) && isCompressionSummary(msg)) {
|
||||||
history = new ArrayList<>(history.subList(i, history.size()));
|
history = new ArrayList<>(history.subList(i, history.size()));
|
||||||
|
boundaryFoundInWindow = true;
|
||||||
log.info("[{}] Found latest compression boundary at index {}; loading {} messages forward",
|
log.info("[{}] Found latest compression boundary at index {}; loading {} messages forward",
|
||||||
agentName, i, history.size());
|
agentName, i, history.size());
|
||||||
break;
|
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 对象 =====
|
// ===== 转换为 Spring AI Message 对象 =====
|
||||||
int limit = history.size();
|
int limit = history.size();
|
||||||
if (limit > 0) {
|
if (limit > 0) {
|
||||||
|
|||||||
@ -464,13 +464,19 @@ public class ConversationWindowManager {
|
|||||||
boundaryMetadata.put("tailKept", recentMessages.size());
|
boundaryMetadata.put("tailKept", recentMessages.size());
|
||||||
boundaryMetadata.put("toolResultsSpilled", spillsThisTurn);
|
boundaryMetadata.put("toolResultsSpilled", spillsThisTurn);
|
||||||
boundaryMetadata.put("anchored", anchored);
|
boundaryMetadata.put("anchored", anchored);
|
||||||
|
Long summaryId = null;
|
||||||
try {
|
try {
|
||||||
conversationService.saveCompressionSummary(
|
summaryId = conversationService.saveCompressionSummaryReturningId(
|
||||||
conversationId, SUMMARY_PREFIX + summary, oldMessages.size(),
|
conversationId, SUMMARY_PREFIX + summary, oldMessages.size(),
|
||||||
boundaryMetadata);
|
boundaryMetadata);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("[ConversationWindow] Failed to persist compression boundary: {}", e.getMessage());
|
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);
|
broadcastCompactStatus(conversationId, "done", boundaryMetadata);
|
||||||
} else if (summary != null && !summary.isBlank() && fromCache) {
|
} else if (summary != null && !summary.isBlank() && fromCache) {
|
||||||
// Cached summary path — no new DB row, but emit done so the
|
// Cached summary path — no new DB row, but emit done so the
|
||||||
|
|||||||
@ -387,6 +387,30 @@ public class ConversationService {
|
|||||||
.orderByAsc(MessageEntity::getId));
|
.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 条消息(倒序取出后翻转为正序)。
|
* 加载最近 N 条消息(倒序取出后翻转为正序)。
|
||||||
* 利用复合索引 (conversation_id, create_time) 高效分页。
|
* 利用复合索引 (conversation_id, create_time) 高效分页。
|
||||||
@ -442,6 +466,24 @@ public class ConversationService {
|
|||||||
saveCompressionSummary(conversationId, summary, compressedCount, Map.of());
|
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
|
* Same as the 3-arg overload but accepts extra structured fields that
|
||||||
* are merged into the boundary's metadata JSON. Fields the frontend
|
* 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,
|
public void saveCompressionSummary(String conversationId, String summary, int compressedCount,
|
||||||
Map<String, Object> extraMetadata) {
|
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();
|
MessageEntity entity = new MessageEntity();
|
||||||
entity.setConversationId(conversationId);
|
entity.setConversationId(conversationId);
|
||||||
entity.setRole("system");
|
entity.setRole("system");
|
||||||
@ -506,6 +553,7 @@ public class ConversationService {
|
|||||||
}
|
}
|
||||||
log.info("[Conversation] Saved compression boundary conv={}, compressedCount={}, metadata={}",
|
log.info("[Conversation] Saved compression boundary conv={}, compressedCount={}, metadata={}",
|
||||||
conversationId, compressedCount, entity.getMetadata());
|
conversationId, compressedCount, entity.getMetadata());
|
||||||
|
return entity.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<MessageVO> listMessageViews(String conversationId) {
|
public List<MessageVO> listMessageViews(String conversationId) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user