mirror of https://github.com/openclaw/openclaw.git
179 lines
6.1 KiB
TypeScript
179 lines
6.1 KiB
TypeScript
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;
|
|
ctx: import("../templating.js").MsgContext;
|
|
cfg: OpenClawConfig;
|
|
agentId?: string;
|
|
isGroup: boolean;
|
|
}): string | undefined {
|
|
const raw = stripStructuralPrefixes(params.rawBody ?? "");
|
|
const stripped = params.isGroup
|
|
? stripMentions(raw, params.ctx, params.cfg, params.agentId)
|
|
: raw;
|
|
const trimmed = stripped.trim();
|
|
if (!trimmed) {
|
|
return undefined;
|
|
}
|
|
const lowered = trimmed.toLowerCase();
|
|
const prefix = lowered.startsWith("/compact") ? "/compact" : null;
|
|
if (!prefix) {
|
|
return undefined;
|
|
}
|
|
let rest = trimmed.slice(prefix.length).trimStart();
|
|
if (rest.startsWith(":")) {
|
|
rest = rest.slice(1).trimStart();
|
|
}
|
|
return rest.length ? rest : undefined;
|
|
}
|
|
|
|
function isCompactionSkipReason(reason?: string): boolean {
|
|
const text = reason?.trim().toLowerCase() ?? "";
|
|
return (
|
|
text.includes("nothing to compact") ||
|
|
text.includes("below threshold") ||
|
|
text.includes("already compacted") ||
|
|
text.includes("no real conversation messages")
|
|
);
|
|
}
|
|
|
|
function formatCompactionReason(reason?: string): string | undefined {
|
|
const text = reason?.trim();
|
|
if (!text) {
|
|
return undefined;
|
|
}
|
|
|
|
const lower = text.toLowerCase();
|
|
if (lower.includes("nothing to compact")) {
|
|
return "nothing compactable in this session yet";
|
|
}
|
|
if (lower.includes("below threshold")) {
|
|
return "context is below the compaction threshold";
|
|
}
|
|
if (lower.includes("already compacted")) {
|
|
return "session was already compacted recently";
|
|
}
|
|
if (lower.includes("no real conversation messages")) {
|
|
return "no real conversation messages yet";
|
|
}
|
|
return text;
|
|
}
|
|
|
|
export const handleCompactCommand: CommandHandler = async (params) => {
|
|
const compactRequested =
|
|
params.command.commandBodyNormalized === "/compact" ||
|
|
params.command.commandBodyNormalized.startsWith("/compact ");
|
|
if (!compactRequested) {
|
|
return null;
|
|
}
|
|
if (!params.command.isAuthorizedSender) {
|
|
logVerbose(
|
|
`Ignoring /compact from unauthorized sender: ${params.command.senderId || "<unknown>"}`,
|
|
);
|
|
return { shouldContinue: false };
|
|
}
|
|
if (!params.sessionEntry?.sessionId) {
|
|
return {
|
|
shouldContinue: false,
|
|
reply: { text: "⚙️ Compaction unavailable (missing session id)." },
|
|
};
|
|
}
|
|
const sessionId = params.sessionEntry.sessionId;
|
|
if (isEmbeddedPiRunActive(sessionId)) {
|
|
abortEmbeddedPiRun(sessionId);
|
|
await waitForEmbeddedPiRunEnd(sessionId, 15_000);
|
|
}
|
|
const customInstructions = extractCompactInstructions({
|
|
rawBody: params.ctx.CommandBody ?? params.ctx.RawBody ?? params.ctx.Body,
|
|
ctx: params.ctx,
|
|
cfg: params.cfg,
|
|
agentId: params.agentId,
|
|
isGroup: params.isGroup,
|
|
});
|
|
const result = await compactEmbeddedPiSession({
|
|
sessionId,
|
|
sessionKey: params.sessionKey,
|
|
allowGatewaySubagentBinding: true,
|
|
messageChannel: params.command.channel,
|
|
groupId: params.sessionEntry.groupId,
|
|
groupChannel: params.sessionEntry.groupChannel,
|
|
groupSpace: params.sessionEntry.space,
|
|
spawnedBy: params.sessionEntry.spawnedBy,
|
|
sessionFile: resolveSessionFilePath(
|
|
sessionId,
|
|
params.sessionEntry,
|
|
resolveSessionFilePathOptions({
|
|
agentId: params.agentId,
|
|
storePath: params.storePath,
|
|
}),
|
|
),
|
|
workspaceDir: params.workspaceDir,
|
|
agentDir: params.agentDir,
|
|
config: params.cfg,
|
|
skillsSnapshot: params.sessionEntry.skillsSnapshot,
|
|
provider: params.provider,
|
|
model: params.model,
|
|
thinkLevel: params.resolvedThinkLevel ?? (await params.resolveDefaultThinkingLevel()),
|
|
bashElevated: {
|
|
enabled: false,
|
|
allowed: false,
|
|
defaultLevel: "off",
|
|
},
|
|
customInstructions,
|
|
trigger: "manual",
|
|
senderIsOwner: params.command.senderIsOwner,
|
|
ownerNumbers: params.command.ownerList.length > 0 ? params.command.ownerList : undefined,
|
|
});
|
|
|
|
const compactLabel =
|
|
result.ok || isCompactionSkipReason(result.reason)
|
|
? result.compacted
|
|
? result.result?.tokensBefore != null && result.result?.tokensAfter != null
|
|
? `Compacted (${formatTokenCount(result.result.tokensBefore)} → ${formatTokenCount(result.result.tokensAfter)})`
|
|
: result.result?.tokensBefore
|
|
? `Compacted (${formatTokenCount(result.result.tokensBefore)} before)`
|
|
: "Compacted"
|
|
: "Compaction skipped"
|
|
: "Compaction failed";
|
|
if (result.ok && result.compacted) {
|
|
await incrementCompactionCount({
|
|
sessionEntry: params.sessionEntry,
|
|
sessionStore: params.sessionStore,
|
|
sessionKey: params.sessionKey,
|
|
storePath: params.storePath,
|
|
// Update token counts after compaction
|
|
tokensAfter: result.result?.tokensAfter,
|
|
});
|
|
}
|
|
// 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(
|
|
typeof totalTokens === "number" && totalTokens > 0 ? totalTokens : null,
|
|
params.contextTokens ?? params.sessionEntry.contextTokens ?? null,
|
|
);
|
|
const reason = formatCompactionReason(result.reason);
|
|
const line = reason
|
|
? `${compactLabel}: ${reason} • ${contextSummary}`
|
|
: `${compactLabel} • ${contextSummary}`;
|
|
enqueueSystemEvent(line, { sessionKey: params.sessionKey });
|
|
return { shouldContinue: false, reply: { text: `⚙️ ${line}` } };
|
|
};
|