mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 22:48:27 +00:00
fix: context overflow compaction and subagent announce improvements (#11664) (thanks @tyler6204)
* initial commit * feat: implement deriveSessionTotalTokens function and update usage tests * Added deriveSessionTotalTokens function to calculate total tokens based on usage and context tokens. * Updated usage tests to include cases for derived session total tokens. * Refactored session usage calculations in multiple files to utilize the new function for improved accuracy. * fix: restore overflow truncation fallback + changelog/test hardening (#11551) (thanks @tyler6204)
This commit is contained in:
@@ -17,7 +17,11 @@ import {
|
||||
mergeDeliveryContext,
|
||||
normalizeDeliveryContext,
|
||||
} from "../utils/delivery-context.js";
|
||||
import { isEmbeddedPiRunActive, queueEmbeddedPiMessage } from "./pi-embedded.js";
|
||||
import {
|
||||
isEmbeddedPiRunActive,
|
||||
queueEmbeddedPiMessage,
|
||||
waitForEmbeddedPiRunEnd,
|
||||
} from "./pi-embedded.js";
|
||||
import { type AnnounceQueueItem, enqueueAnnounce } from "./subagent-announce-queue.js";
|
||||
import { readLatestAssistantReply } from "./tools/agent-step.js";
|
||||
|
||||
@@ -288,6 +292,35 @@ async function buildSubagentStatsLine(params: {
|
||||
return `Stats: ${parts.join(" \u2022 ")}`;
|
||||
}
|
||||
|
||||
function loadSessionEntryByKey(sessionKey: string) {
|
||||
const cfg = loadConfig();
|
||||
const agentId = resolveAgentIdFromSessionKey(sessionKey);
|
||||
const storePath = resolveStorePath(cfg.session?.store, { agentId });
|
||||
const store = loadSessionStore(storePath);
|
||||
return store[sessionKey];
|
||||
}
|
||||
|
||||
async function readLatestAssistantReplyWithRetry(params: {
|
||||
sessionKey: string;
|
||||
initialReply?: string;
|
||||
maxWaitMs: number;
|
||||
}): Promise<string | undefined> {
|
||||
let reply = params.initialReply?.trim() ? params.initialReply : undefined;
|
||||
if (reply) {
|
||||
return reply;
|
||||
}
|
||||
|
||||
const deadline = Date.now() + Math.max(0, Math.min(params.maxWaitMs, 15_000));
|
||||
while (Date.now() < deadline) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
const latest = await readLatestAssistantReply({ sessionKey: params.sessionKey });
|
||||
if (latest?.trim()) {
|
||||
return latest;
|
||||
}
|
||||
}
|
||||
return reply;
|
||||
}
|
||||
|
||||
export function buildSubagentSystemPrompt(params: {
|
||||
requesterSessionKey?: string;
|
||||
requesterOrigin?: DeliveryContext;
|
||||
@@ -365,12 +398,33 @@ export async function runSubagentAnnounceFlow(params: {
|
||||
announceType?: SubagentAnnounceType;
|
||||
}): Promise<boolean> {
|
||||
let didAnnounce = false;
|
||||
let shouldDeleteChildSession = params.cleanup === "delete";
|
||||
try {
|
||||
const requesterOrigin = normalizeDeliveryContext(params.requesterOrigin);
|
||||
const childSessionId = (() => {
|
||||
const entry = loadSessionEntryByKey(params.childSessionKey);
|
||||
return typeof entry?.sessionId === "string" && entry.sessionId.trim()
|
||||
? entry.sessionId.trim()
|
||||
: undefined;
|
||||
})();
|
||||
const settleTimeoutMs = Math.min(Math.max(params.timeoutMs, 1), 120_000);
|
||||
let reply = params.roundOneReply;
|
||||
let outcome: SubagentRunOutcome | undefined = params.outcome;
|
||||
// Lifecycle "end" can arrive before auto-compaction retries finish. If the
|
||||
// subagent is still active, wait for the embedded run to fully settle.
|
||||
if (childSessionId && isEmbeddedPiRunActive(childSessionId)) {
|
||||
const settled = await waitForEmbeddedPiRunEnd(childSessionId, settleTimeoutMs);
|
||||
if (!settled && isEmbeddedPiRunActive(childSessionId)) {
|
||||
// The child run is still active (e.g., compaction retry still in progress).
|
||||
// Defer announcement so we don't report stale/partial output.
|
||||
// Keep the child session so output is not lost while the run is still active.
|
||||
shouldDeleteChildSession = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!reply && params.waitForCompletion !== false) {
|
||||
const waitMs = Math.min(params.timeoutMs, 60_000);
|
||||
const waitMs = settleTimeoutMs;
|
||||
const wait = await callGateway<{
|
||||
status?: string;
|
||||
startedAt?: number;
|
||||
@@ -403,17 +457,27 @@ export async function runSubagentAnnounceFlow(params: {
|
||||
outcome = { status: "timeout" };
|
||||
}
|
||||
}
|
||||
reply = await readLatestAssistantReply({
|
||||
sessionKey: params.childSessionKey,
|
||||
});
|
||||
reply = await readLatestAssistantReply({ sessionKey: params.childSessionKey });
|
||||
}
|
||||
|
||||
if (!reply) {
|
||||
reply = await readLatestAssistantReply({
|
||||
reply = await readLatestAssistantReply({ sessionKey: params.childSessionKey });
|
||||
}
|
||||
|
||||
if (!reply?.trim()) {
|
||||
reply = await readLatestAssistantReplyWithRetry({
|
||||
sessionKey: params.childSessionKey,
|
||||
initialReply: reply,
|
||||
maxWaitMs: params.timeoutMs,
|
||||
});
|
||||
}
|
||||
|
||||
if (!reply?.trim() && childSessionId && isEmbeddedPiRunActive(childSessionId)) {
|
||||
// Avoid announcing "(no output)" while the child run is still producing output.
|
||||
shouldDeleteChildSession = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!outcome) {
|
||||
outcome = { status: "unknown" };
|
||||
}
|
||||
@@ -508,7 +572,7 @@ export async function runSubagentAnnounceFlow(params: {
|
||||
// Best-effort
|
||||
}
|
||||
}
|
||||
if (params.cleanup === "delete") {
|
||||
if (shouldDeleteChildSession) {
|
||||
try {
|
||||
await callGateway({
|
||||
method: "sessions.delete",
|
||||
|
||||
Reference in New Issue
Block a user