mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 11:01:24 +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:
@@ -1,6 +1,8 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const agentSpy = vi.fn(async () => ({ runId: "run-main", status: "ok" }));
|
||||
const sessionsDeleteSpy = vi.fn();
|
||||
const readLatestAssistantReplyMock = vi.fn(async () => "raw subagent reply");
|
||||
const embeddedRunMock = {
|
||||
isEmbeddedPiRunActive: vi.fn(() => false),
|
||||
isEmbeddedPiRunStreaming: vi.fn(() => false),
|
||||
@@ -28,6 +30,7 @@ vi.mock("../gateway/call.js", () => ({
|
||||
return {};
|
||||
}
|
||||
if (typed.method === "sessions.delete") {
|
||||
sessionsDeleteSpy(typed);
|
||||
return {};
|
||||
}
|
||||
return {};
|
||||
@@ -35,7 +38,7 @@ vi.mock("../gateway/call.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./tools/agent-step.js", () => ({
|
||||
readLatestAssistantReply: vi.fn(async () => "raw subagent reply"),
|
||||
readLatestAssistantReply: readLatestAssistantReplyMock,
|
||||
}));
|
||||
|
||||
vi.mock("../config/sessions.js", () => ({
|
||||
@@ -60,10 +63,12 @@ vi.mock("../config/config.js", async (importOriginal) => {
|
||||
describe("subagent announce formatting", () => {
|
||||
beforeEach(() => {
|
||||
agentSpy.mockClear();
|
||||
sessionsDeleteSpy.mockClear();
|
||||
embeddedRunMock.isEmbeddedPiRunActive.mockReset().mockReturnValue(false);
|
||||
embeddedRunMock.isEmbeddedPiRunStreaming.mockReset().mockReturnValue(false);
|
||||
embeddedRunMock.queueEmbeddedPiMessage.mockReset().mockReturnValue(false);
|
||||
embeddedRunMock.waitForEmbeddedPiRunEnd.mockReset().mockResolvedValue(true);
|
||||
readLatestAssistantReplyMock.mockReset().mockResolvedValue("raw subagent reply");
|
||||
sessionStore = {};
|
||||
configOverride = {
|
||||
session: {
|
||||
@@ -356,6 +361,95 @@ describe("subagent announce formatting", () => {
|
||||
expect(call?.params?.accountId).toBe("acct-123");
|
||||
});
|
||||
|
||||
it("retries reading subagent output when early lifecycle completion had no text", async () => {
|
||||
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
|
||||
embeddedRunMock.isEmbeddedPiRunActive.mockReturnValueOnce(true).mockReturnValue(false);
|
||||
embeddedRunMock.waitForEmbeddedPiRunEnd.mockResolvedValue(true);
|
||||
readLatestAssistantReplyMock
|
||||
.mockResolvedValueOnce(undefined)
|
||||
.mockResolvedValueOnce("Read #12 complete.");
|
||||
sessionStore = {
|
||||
"agent:main:subagent:test": {
|
||||
sessionId: "child-session-1",
|
||||
},
|
||||
};
|
||||
|
||||
await runSubagentAnnounceFlow({
|
||||
childSessionKey: "agent:main:subagent:test",
|
||||
childRunId: "run-child",
|
||||
requesterSessionKey: "agent:main:main",
|
||||
requesterDisplayKey: "main",
|
||||
task: "context-stress-test",
|
||||
timeoutMs: 1000,
|
||||
cleanup: "keep",
|
||||
waitForCompletion: false,
|
||||
startedAt: 10,
|
||||
endedAt: 20,
|
||||
outcome: { status: "ok" },
|
||||
});
|
||||
|
||||
expect(embeddedRunMock.waitForEmbeddedPiRunEnd).toHaveBeenCalledWith("child-session-1", 1000);
|
||||
const call = agentSpy.mock.calls[0]?.[0] as { params?: { message?: string } };
|
||||
expect(call?.params?.message).toContain("Read #12 complete.");
|
||||
expect(call?.params?.message).not.toContain("(no output)");
|
||||
});
|
||||
|
||||
it("defers announce when child run is still active after wait timeout", async () => {
|
||||
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
|
||||
embeddedRunMock.isEmbeddedPiRunActive.mockReturnValue(true);
|
||||
embeddedRunMock.waitForEmbeddedPiRunEnd.mockResolvedValue(false);
|
||||
sessionStore = {
|
||||
"agent:main:subagent:test": {
|
||||
sessionId: "child-session-active",
|
||||
},
|
||||
};
|
||||
|
||||
const didAnnounce = await runSubagentAnnounceFlow({
|
||||
childSessionKey: "agent:main:subagent:test",
|
||||
childRunId: "run-child-active",
|
||||
requesterSessionKey: "agent:main:main",
|
||||
requesterDisplayKey: "main",
|
||||
task: "context-stress-test",
|
||||
timeoutMs: 1000,
|
||||
cleanup: "keep",
|
||||
waitForCompletion: false,
|
||||
startedAt: 10,
|
||||
endedAt: 20,
|
||||
outcome: { status: "ok" },
|
||||
});
|
||||
|
||||
expect(didAnnounce).toBe(false);
|
||||
expect(agentSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not delete child session when announce is deferred for an active run", async () => {
|
||||
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
|
||||
embeddedRunMock.isEmbeddedPiRunActive.mockReturnValue(true);
|
||||
embeddedRunMock.waitForEmbeddedPiRunEnd.mockResolvedValue(false);
|
||||
sessionStore = {
|
||||
"agent:main:subagent:test": {
|
||||
sessionId: "child-session-active",
|
||||
},
|
||||
};
|
||||
|
||||
const didAnnounce = await runSubagentAnnounceFlow({
|
||||
childSessionKey: "agent:main:subagent:test",
|
||||
childRunId: "run-child-active-delete",
|
||||
requesterSessionKey: "agent:main:main",
|
||||
requesterDisplayKey: "main",
|
||||
task: "context-stress-test",
|
||||
timeoutMs: 1000,
|
||||
cleanup: "delete",
|
||||
waitForCompletion: false,
|
||||
startedAt: 10,
|
||||
endedAt: 20,
|
||||
outcome: { status: "ok" },
|
||||
});
|
||||
|
||||
expect(didAnnounce).toBe(false);
|
||||
expect(sessionsDeleteSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("normalizes requesterOrigin for direct announce delivery", async () => {
|
||||
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
|
||||
embeddedRunMock.isEmbeddedPiRunActive.mockReturnValue(false);
|
||||
|
||||
Reference in New Issue
Block a user