diff --git a/mateclaw-server/src/main/java/vip/mate/agent/context/ConversationWindowManager.java b/mateclaw-server/src/main/java/vip/mate/agent/context/ConversationWindowManager.java index 52eb6971..bda69ee9 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/context/ConversationWindowManager.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/context/ConversationWindowManager.java @@ -74,6 +74,20 @@ public class ConversationWindowManager { private static final int CONTENT_TAIL = 1500; private static final int OLD_TOOL_RESULT_SUMMARY_THRESHOLD = 500; + /** + * Tool names whose results must never be compacted into a one-line + * summary. Sub-agent delegations are irreplaceable: the child runs an + * independent LLM session that the parent cannot reproduce, so dropping + * earlier batches forces the parent to re-dispatch the same children to + * recover what was lost. Every other tool (read_file, shell, search, + * memory) can be re-invoked cheaply if the parent decides it needs + * the data again. + */ + private static final java.util.Set PRUNE_EXEMPT_TOOLS = java.util.Set.of( + "delegateToAgent", + "delegateParallel" + ); + // ==================== 冷却机制 ==================== /** 摘要失败后的冷却时间(毫秒):10 分钟 */ @@ -384,7 +398,8 @@ public class ConversationWindowManager { boolean messageChanged = false; for (ToolResponseMessage.ToolResponse r : trm.getResponses()) { String data = r.responseData(); - if (keepFull || data == null || data.length() <= OLD_TOOL_RESULT_SUMMARY_THRESHOLD) { + boolean exempt = r.name() != null && PRUNE_EXEMPT_TOOLS.contains(r.name()); + if (keepFull || exempt || data == null || data.length() <= OLD_TOOL_RESULT_SUMMARY_THRESHOLD) { newResponses.add(r); if (data != null && data.length() > OLD_TOOL_RESULT_SUMMARY_THRESHOLD) { seenLargeOutputs.add(data); diff --git a/mateclaw-server/src/main/java/vip/mate/agent/runtime/AgentRuntimeController.java b/mateclaw-server/src/main/java/vip/mate/agent/runtime/AgentRuntimeController.java index 15b9597a..867af4a1 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/runtime/AgentRuntimeController.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/runtime/AgentRuntimeController.java @@ -12,6 +12,7 @@ import vip.mate.audit.service.AuditEventService; import vip.mate.channel.web.ChatStreamTracker; import vip.mate.common.result.R; import vip.mate.exception.MateClawException; +import vip.mate.workspace.conversation.ConversationService; import java.util.LinkedHashMap; import java.util.List; @@ -35,6 +36,7 @@ public class AgentRuntimeController { private final ChatStreamTracker streamTracker; private final SubagentRegistry subagentRegistry; private final AuditEventService auditEventService; + private final ConversationService conversationService; @Operation(summary = "Snapshot of every in-flight agent turn") @GetMapping("/snapshot") @@ -59,6 +61,18 @@ public class AgentRuntimeController { Authentication auth) { requireAdmin(auth); boolean ok = streamTracker.forceRecycle(conversationId); + if (ok) { + // Flip stream_status off 'running' so the sidebar drops the + // 生成中 badge immediately. The downstream doOnCancel / + // doOnComplete still fires later (when the agent yields) and may + // re-set this to 'idle' — same value, no-op. + try { + conversationService.updateStreamStatus(conversationId, "idle"); + } catch (Exception e) { + log.warn("recycle: failed to reset stream_status for {}: {}", + conversationId, e.getMessage()); + } + } recordAudit(auth, "agent-runtime.recycle", conversationId, Map.of("result", ok)); return R.ok(Map.of("recycled", ok)); } @@ -89,7 +103,15 @@ public class AgentRuntimeController { .toList(); int recycled = 0; for (String cid : ids) { - if (streamTracker.forceRecycle(cid)) recycled++; + if (streamTracker.forceRecycle(cid)) { + recycled++; + try { + conversationService.updateStreamStatus(cid, "idle"); + } catch (Exception e) { + log.warn("sweep: failed to reset stream_status for {}: {}", + cid, e.getMessage()); + } + } } recordAudit(auth, "agent-runtime.sweep", "all", Map.of("targets", ids, "recycled", recycled)); diff --git a/mateclaw-server/src/main/java/vip/mate/channel/web/ChatStreamTracker.java b/mateclaw-server/src/main/java/vip/mate/channel/web/ChatStreamTracker.java index d5c297a9..d0f32f3c 100644 --- a/mateclaw-server/src/main/java/vip/mate/channel/web/ChatStreamTracker.java +++ b/mateclaw-server/src/main/java/vip/mate/channel/web/ChatStreamTracker.java @@ -1603,6 +1603,18 @@ public class ChatStreamTracker { public boolean forceRecycle(String conversationId) { RunState state = runs.get(conversationId); if (state == null) return false; + // Persist any partial assistant content first — dispose() only severs + // the downstream subscription, the agent's worker thread keeps running + // and may not yield for minutes. Without this, the conversation row + // shows only the user message until the late doOnComplete fires. + Runnable callback = state.emergencySaveCallback; + if (callback != null) { + try { + callback.run(); + } catch (Exception e) { + log.warn("forceRecycle: emergency save failed for {}: {}", conversationId, e.getMessage()); + } + } try { state.stopRequested.set(true); state.interruptType = InterruptType.USER_STOP; diff --git a/mateclaw-server/src/main/java/vip/mate/tool/builtin/DelegateAgentTool.java b/mateclaw-server/src/main/java/vip/mate/tool/builtin/DelegateAgentTool.java index 7e113d80..261e3bf9 100644 --- a/mateclaw-server/src/main/java/vip/mate/tool/builtin/DelegateAgentTool.java +++ b/mateclaw-server/src/main/java/vip/mate/tool/builtin/DelegateAgentTool.java @@ -46,7 +46,16 @@ public class DelegateAgentTool { private static final int MAX_DELEGATION_DEPTH = 3; private static final int MAX_RESULT_LENGTH = 4000; - private static final int MAX_PARALLEL_CHILDREN = 3; + /** + * Cap on children dispatched in a single delegateParallel call. Set to 8 + * because real multi-role evaluations commonly cover 5-8 perspectives + * (architecture / backend / frontend / security / cost / ops / contrarian / + * progressive-alternative); a lower cap forces the parent to split into + * batches, and once an older batch's tool result gets compacted by + * ConversationWindowManager the parent can no longer reconstruct what each + * child said and starts re-dispatching the same roles in a loop. + */ + private static final int MAX_PARALLEL_CHILDREN = 8; /** * RFC-03 Lane C2 — caps for the parent-context prefix when @@ -425,6 +434,14 @@ public class DelegateAgentTool { results.add(ChildResult.ofError(idx, agentName, ex.getMessage())); } } else { + // Signal the child's graph to bail out at its next checkpoint. + // CompletableFuture.cancel alone only marks the future as + // cancelled; without requestStop the underlying ReAct/Plan-Execute + // loop keeps invoking LLMs and tools (observed: 8-minute orphan + // child still writing files long after the parent gave up). + if (p != null && p.childConvId != null) { + streamTracker.requestStop(p.childConvId); + } f.cancel(true); // Use ofTimeout so outcome="timeout" is explicit and distinct from "error". results.add(ChildResult.ofTimeout(idx, agentName, PARALLEL_TIMEOUT_SECONDS));