import { resolveAgentDir, resolveSessionAgentId } from "../../agents/agent-scope.js" ;
import type { OpenClawConfig } from "../../config/types.openclaw.js" ;
import { logVerbose } from "../../globals.js" ;
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
normalizeOptionalString,
} from "../../shared/string-coerce.js" ;
import type { CommandHandler } from "./commands-types.js" ;
import { stripMentions, stripStructuralPrefixes } from "./mentions.js" ;
let compactRuntimePromise: Promise<typeof import ("./commands-compact.runtime.js" )> | null = null ;
function loadCompactRuntime(): Promise<typeof import ("./commands-compact.runtime.js" )> {
compactRuntimePromise ??= import ("./commands-compact.runtime.js" );
return compactRuntimePromise;
}
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 = normalizeLowercaseStringOrEmpty(trimmed);
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 = normalizeOptionalLowercaseString(reason) ?? "" ;
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 = normalizeOptionalString(reason);
if (!text) {
return undefined;
}
const lower = normalizeLowercaseStringOrEmpty(text);
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 };
}
const targetSessionEntry = params.sessionStore?.[params.sessionKey] ?? params.sessionEntry;
if (!targetSessionEntry?.sessionId) {
return {
shouldContinue: false ,
reply: { text: "⚙️ Compaction unavailable (missing session id)." },
};
}
const runtime = await loadCompactRuntime();
const sessionId = targetSessionEntry.sessionId;
if (runtime.isEmbeddedPiRunActive(sessionId)) {
runtime.abortEmbeddedPiRun(sessionId);
await runtime.waitForEmbeddedPiRunEnd(sessionId, 15 _000 );
}
const sessionAgentId = params.sessionKey
? resolveSessionAgentId({ sessionKey: params.sessionKey, config: params.cfg })
: (params.agentId ?? "main" );
const currentAgentId = params.agentId ?? "main" ;
const sessionAgentDir =
sessionAgentId === currentAgentId && params.agentDir
? params.agentDir
: resolveAgentDir(params.cfg, sessionAgentId);
const customInstructions = extractCompactInstructions({
rawBody: params.ctx.CommandBody ?? params.ctx.RawBody ?? params.ctx.Body,
ctx: params.ctx,
cfg: params.cfg,
agentId: sessionAgentId,
isGroup: params.isGroup,
});
const result = await runtime.compactEmbeddedPiSession({
sessionId,
sessionKey: params.sessionKey,
allowGatewaySubagentBinding: true ,
messageChannel: params.command.channel,
groupId: targetSessionEntry.groupId,
groupChannel: targetSessionEntry.groupChannel,
groupSpace: targetSessionEntry.space,
spawnedBy: targetSessionEntry.spawnedBy,
senderId: params.command.senderId,
senderName: params.ctx.SenderName,
senderUsername: params.ctx.SenderUsername,
senderE164: params.ctx.SenderE164,
sessionFile: runtime.resolveSessionFilePath(
sessionId,
targetSessionEntry,
runtime.resolveSessionFilePathOptions({
agentId: sessionAgentId,
storePath: params.storePath,
}),
),
workspaceDir: params.workspaceDir,
agentDir: sessionAgentDir,
config: params.cfg,
skillsSnapshot: targetSessionEntry.skillsSnapshot,
provider: params.provider,
model: params.model,
agentHarnessId:
targetSessionEntry.sessionId === sessionId ? targetSessionEntry.agentHarnessId : undefined,
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 (${runtime.formatTokenCount(result.result.tokensBefore)} → ${runtime.formatTokenCount(result.result.tokensAfter)})`
: result.result?.tokensBefore
? `Compacted (${runtime.formatTokenCount(result.result.tokensBefore)} before)`
: "Compacted"
: "Compaction skipped"
: "Compaction failed" ;
if (result.ok && result.compacted) {
await runtime.incrementCompactionCount({
cfg: params.cfg,
sessionEntry: targetSessionEntry,
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 ?? runtime.resolveFreshSessionTotalTokens(targetSessionEntry);
const contextSummary = runtime.formatContextUsageShort(
typeof totalTokens === "number" && totalTokens > 0 ? totalTokens : null ,
params.contextTokens ?? targetSessionEntry.contextTokens ?? null ,
);
const reason = formatCompactionReason(result.reason);
const line = reason
? `${compactLabel}: ${reason} • ${contextSummary}`
: `${compactLabel} • ${contextSummary}`;
runtime.enqueueSystemEvent(line, { sessionKey: params.sessionKey });
return { shouldContinue: false , reply: { text: `⚙️ ${line}` } };
};
Messung V0.5 in Prozent C=99 H=100 G=99
¤ Dauer der Verarbeitung: 0.10 Sekunden
(vorverarbeitet am 2026-06-08)
¤
*© Formatika GbR, Deutschland