Files
Moltbot/src/auto-reply/reply/commands-compact.ts
Gustavo Madeira Santana 392bbddf29 Security: owner-only tools + command auth hardening (#9202)
* Security: gate whatsapp_login by sender auth

* Security: treat undefined senderAuthorized as unauthorized (opt-in)

* fix: gate whatsapp_login to owner senders (#8768) (thanks @victormier)

* fix: add explicit owner allowlist for tools (#8768) (thanks @victormier)

* fix: normalize escaped newlines in send actions (#8768) (thanks @victormier)

---------

Co-authored-by: Victor Mier <victormier@gmail.com>
2026-02-04 19:49:36 -05:00

135 lines
4.9 KiB
TypeScript

import type { OpenClawConfig } from "../../config/config.js";
import type { CommandHandler } from "./commands-types.js";
import {
abortEmbeddedPiRun,
compactEmbeddedPiSession,
isEmbeddedPiRunActive,
waitForEmbeddedPiRunEnd,
} from "../../agents/pi-embedded.js";
import { resolveSessionFilePath } from "../../config/sessions.js";
import { logVerbose } from "../../globals.js";
import { enqueueSystemEvent } from "../../infra/system-events.js";
import { formatContextUsageShort, formatTokenCount } from "../status.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;
}
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,
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),
workspaceDir: params.workspaceDir,
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,
senderIsOwner: params.command.senderIsOwner,
ownerNumbers: params.command.ownerList.length > 0 ? params.command.ownerList : undefined,
});
const compactLabel = result.ok
? 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 ??
params.sessionEntry.totalTokens ??
(params.sessionEntry.inputTokens ?? 0) + (params.sessionEntry.outputTokens ?? 0);
const contextSummary = formatContextUsageShort(
totalTokens > 0 ? totalTokens : null,
params.contextTokens ?? params.sessionEntry.contextTokens ?? null,
);
const reason = result.reason?.trim();
const line = reason
? `${compactLabel}: ${reason}${contextSummary}`
: `${compactLabel}${contextSummary}`;
enqueueSystemEvent(line, { sessionKey: params.sessionKey });
return { shouldContinue: false, reply: { text: `⚙️ ${line}` } };
};