perf(reply): lazy load compact runtime

This commit is contained in:
Peter Steinberger
2026-04-06 04:58:34 +01:00
parent b40e28f76e
commit 8796a82ce4
3 changed files with 169 additions and 26 deletions

View File

@@ -0,0 +1,14 @@
export {
abortEmbeddedPiRun,
compactEmbeddedPiSession,
isEmbeddedPiRunActive,
waitForEmbeddedPiRunEnd,
} from "../../agents/pi-embedded.js";
export {
resolveFreshSessionTotalTokens,
resolveSessionFilePath,
resolveSessionFilePathOptions,
} from "../../config/sessions.js";
export { enqueueSystemEvent } from "../../infra/system-events.js";
export { formatContextUsageShort, formatTokenCount } from "../status.js";
export { incrementCompactionCount } from "./session-updates.js";

View File

@@ -0,0 +1,141 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import { handleCompactCommand } from "./commands-compact.js";
import type { HandleCommandsParams } from "./commands-types.js";
vi.mock("./commands-compact.runtime.js", () => ({
abortEmbeddedPiRun: vi.fn(),
compactEmbeddedPiSession: vi.fn(),
enqueueSystemEvent: vi.fn(),
formatContextUsageShort: vi.fn(() => "Context 12.1k"),
formatTokenCount: vi.fn((value: number) => `${value}`),
incrementCompactionCount: vi.fn(),
isEmbeddedPiRunActive: vi.fn().mockReturnValue(false),
resolveFreshSessionTotalTokens: vi.fn(() => 12_345),
resolveSessionFilePath: vi.fn(() => "/tmp/session.json"),
resolveSessionFilePathOptions: vi.fn(() => ({})),
waitForEmbeddedPiRunEnd: vi.fn().mockResolvedValue(undefined),
}));
const { compactEmbeddedPiSession } = await import("./commands-compact.runtime.js");
function buildCompactParams(
commandBodyNormalized: string,
cfg: OpenClawConfig,
): HandleCommandsParams {
return {
cfg,
ctx: {
Provider: "whatsapp",
Surface: "whatsapp",
CommandSource: "text",
CommandBody: commandBodyNormalized,
},
command: {
commandBodyNormalized,
isAuthorizedSender: true,
senderIsOwner: false,
senderId: "owner",
channel: "whatsapp",
ownerList: [],
},
sessionKey: "agent:main:main",
sessionStore: {},
resolveDefaultThinkingLevel: async () => "medium",
} as unknown as HandleCommandsParams;
}
describe("handleCompactCommand", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns null when command is not /compact", async () => {
const result = await handleCompactCommand(
buildCompactParams("/status", {
commands: { text: true },
channels: { whatsapp: { allowFrom: ["*"] } },
} as OpenClawConfig),
true,
);
expect(result).toBeNull();
expect(vi.mocked(compactEmbeddedPiSession)).not.toHaveBeenCalled();
});
it("rejects unauthorized /compact commands", async () => {
const params = buildCompactParams("/compact", {
commands: { text: true },
channels: { whatsapp: { allowFrom: ["*"] } },
} as OpenClawConfig);
const result = await handleCompactCommand(
{
...params,
command: {
...params.command,
isAuthorizedSender: false,
senderId: "unauthorized",
},
} as HandleCommandsParams,
true,
);
expect(result).toEqual({ shouldContinue: false });
expect(vi.mocked(compactEmbeddedPiSession)).not.toHaveBeenCalled();
});
it("routes manual compaction with explicit trigger and context metadata", async () => {
vi.mocked(compactEmbeddedPiSession).mockResolvedValueOnce({
ok: true,
compacted: false,
});
const result = await handleCompactCommand(
{
...buildCompactParams("/compact", {
commands: { text: true },
channels: { whatsapp: { allowFrom: ["*"] } },
session: { store: "/tmp/openclaw-session-store.json" },
} as OpenClawConfig),
ctx: {
Provider: "whatsapp",
Surface: "whatsapp",
CommandSource: "text",
CommandBody: "/compact: focus on decisions",
From: "+15550001",
To: "+15550002",
},
agentDir: "/tmp/openclaw-agent-compact",
sessionEntry: {
sessionId: "session-1",
updatedAt: Date.now(),
groupId: "group-1",
groupChannel: "#general",
space: "workspace-1",
spawnedBy: "agent:main:parent",
totalTokens: 12345,
},
} as HandleCommandsParams,
true,
);
expect(result?.shouldContinue).toBe(false);
expect(vi.mocked(compactEmbeddedPiSession)).toHaveBeenCalledOnce();
expect(vi.mocked(compactEmbeddedPiSession)).toHaveBeenCalledWith(
expect.objectContaining({
sessionId: "session-1",
sessionKey: "agent:main:main",
allowGatewaySubagentBinding: true,
trigger: "manual",
customInstructions: "focus on decisions",
messageChannel: "whatsapp",
groupId: "group-1",
groupChannel: "#general",
groupSpace: "workspace-1",
spawnedBy: "agent:main:parent",
agentDir: "/tmp/openclaw-agent-compact",
}),
);
});
});

View File

@@ -1,21 +1,7 @@
import {
abortEmbeddedPiRun,
compactEmbeddedPiSession,
isEmbeddedPiRunActive,
waitForEmbeddedPiRunEnd,
} from "../../agents/pi-embedded.js";
import type { OpenClawConfig } from "../../config/config.js";
import {
resolveFreshSessionTotalTokens,
resolveSessionFilePath,
resolveSessionFilePathOptions,
} from "../../config/sessions.js";
import { logVerbose } from "../../globals.js";
import { enqueueSystemEvent } from "../../infra/system-events.js";
import { formatContextUsageShort, formatTokenCount } from "../status.js";
import type { CommandHandler } from "./commands-types.js";
import { stripMentions, stripStructuralPrefixes } from "./mentions.js";
import { incrementCompactionCount } from "./session-updates.js";
function extractCompactInstructions(params: {
rawBody?: string;
@@ -95,10 +81,11 @@ export const handleCompactCommand: CommandHandler = async (params) => {
reply: { text: "⚙️ Compaction unavailable (missing session id)." },
};
}
const runtime = await import("./commands-compact.runtime.js");
const sessionId = params.sessionEntry.sessionId;
if (isEmbeddedPiRunActive(sessionId)) {
abortEmbeddedPiRun(sessionId);
await waitForEmbeddedPiRunEnd(sessionId, 15_000);
if (runtime.isEmbeddedPiRunActive(sessionId)) {
runtime.abortEmbeddedPiRun(sessionId);
await runtime.waitForEmbeddedPiRunEnd(sessionId, 15_000);
}
const customInstructions = extractCompactInstructions({
rawBody: params.ctx.CommandBody ?? params.ctx.RawBody ?? params.ctx.Body,
@@ -107,7 +94,7 @@ export const handleCompactCommand: CommandHandler = async (params) => {
agentId: params.agentId,
isGroup: params.isGroup,
});
const result = await compactEmbeddedPiSession({
const result = await runtime.compactEmbeddedPiSession({
sessionId,
sessionKey: params.sessionKey,
allowGatewaySubagentBinding: true,
@@ -116,10 +103,10 @@ export const handleCompactCommand: CommandHandler = async (params) => {
groupChannel: params.sessionEntry.groupChannel,
groupSpace: params.sessionEntry.space,
spawnedBy: params.sessionEntry.spawnedBy,
sessionFile: resolveSessionFilePath(
sessionFile: runtime.resolveSessionFilePath(
sessionId,
params.sessionEntry,
resolveSessionFilePathOptions({
runtime.resolveSessionFilePathOptions({
agentId: params.agentId,
storePath: params.storePath,
}),
@@ -146,14 +133,14 @@ export const handleCompactCommand: CommandHandler = async (params) => {
result.ok || isCompactionSkipReason(result.reason)
? result.compacted
? result.result?.tokensBefore != null && result.result?.tokensAfter != null
? `Compacted (${formatTokenCount(result.result.tokensBefore)}${formatTokenCount(result.result.tokensAfter)})`
? `Compacted (${runtime.formatTokenCount(result.result.tokensBefore)}${runtime.formatTokenCount(result.result.tokensAfter)})`
: result.result?.tokensBefore
? `Compacted (${formatTokenCount(result.result.tokensBefore)} before)`
? `Compacted (${runtime.formatTokenCount(result.result.tokensBefore)} before)`
: "Compacted"
: "Compaction skipped"
: "Compaction failed";
if (result.ok && result.compacted) {
await incrementCompactionCount({
await runtime.incrementCompactionCount({
cfg: params.cfg,
sessionEntry: params.sessionEntry,
sessionStore: params.sessionStore,
@@ -165,8 +152,9 @@ export const handleCompactCommand: CommandHandler = async (params) => {
}
// Use the post-compaction token count for context summary if available
const tokensAfterCompaction = result.result?.tokensAfter;
const totalTokens = tokensAfterCompaction ?? resolveFreshSessionTotalTokens(params.sessionEntry);
const contextSummary = formatContextUsageShort(
const totalTokens =
tokensAfterCompaction ?? runtime.resolveFreshSessionTotalTokens(params.sessionEntry);
const contextSummary = runtime.formatContextUsageShort(
typeof totalTokens === "number" && totalTokens > 0 ? totalTokens : null,
params.contextTokens ?? params.sessionEntry.contextTokens ?? null,
);
@@ -174,6 +162,6 @@ export const handleCompactCommand: CommandHandler = async (params) => {
const line = reason
? `${compactLabel}: ${reason}${contextSummary}`
: `${compactLabel}${contextSummary}`;
enqueueSystemEvent(line, { sessionKey: params.sessionKey });
runtime.enqueueSystemEvent(line, { sessionKey: params.sessionKey });
return { shouldContinue: false, reply: { text: `⚙️ ${line}` } };
};