998 lines
30 KiB
TypeScript
Raw Normal View History

import {
resolveAgentDir,
resolveDefaultAgentId,
resolveSessionAgentId,
} from "../../agents/agent-scope.js";
import {
ensureAuthProfileStore,
2026-01-09 03:38:33 +01:00
resolveAuthProfileDisplayLabel,
resolveAuthProfileOrder,
} from "../../agents/auth-profiles.js";
import {
getCustomProviderApiKey,
resolveEnvApiKey,
} from "../../agents/model-auth.js";
import { normalizeProviderId } from "../../agents/model-selection.js";
import {
abortEmbeddedPiRun,
compactEmbeddedPiSession,
isEmbeddedPiRunActive,
waitForEmbeddedPiRunEnd,
} from "../../agents/pi-embedded.js";
2026-01-04 14:32:47 +00:00
import type { ClawdbotConfig } from "../../config/config.js";
2026-01-10 03:00:24 +01:00
import {
readConfigFileSnapshot,
validateConfigObject,
writeConfigFile,
} from "../../config/config.js";
import {
getConfigValueAtPath,
parseConfigPath,
setConfigValueAtPath,
unsetConfigValueAtPath,
} from "../../config/config-paths.js";
2026-01-04 07:05:04 +01:00
import {
2026-01-09 16:55:51 +01:00
getConfigOverrides,
resetConfigOverrides,
setConfigOverride,
unsetConfigOverride,
} from "../../config/runtime-overrides.js";
2026-01-04 07:05:04 +01:00
import {
resolveSessionFilePath,
2026-01-04 07:05:04 +01:00
type SessionEntry,
type SessionScope,
saveSessionStore,
} from "../../config/sessions.js";
2026-01-04 05:47:21 +01:00
import { logVerbose } from "../../globals.js";
2026-01-07 11:48:53 +01:00
import {
formatUsageSummaryLine,
loadProviderUsageSummary,
2026-01-09 03:30:04 +01:00
resolveUsageProviderId,
2026-01-07 11:48:53 +01:00
} from "../../infra/provider-usage.js";
2026-01-08 06:48:28 +00:00
import {
scheduleGatewaySigusr1Restart,
triggerClawdbotRestart,
} from "../../infra/restart.js";
import { enqueueSystemEvent } from "../../infra/system-events.js";
import { parseAgentSessionKey } from "../../routing/session-key.js";
2026-01-04 05:47:21 +01:00
import { resolveSendPolicy } from "../../sessions/send-policy.js";
import { normalizeE164 } from "../../utils.js";
import { resolveCommandAuthorization } from "../command-auth.js";
2026-01-08 03:22:14 +01:00
import {
normalizeCommandBody,
shouldHandleTextCommands,
} from "../commands-registry.js";
2026-01-04 05:47:21 +01:00
import {
normalizeGroupActivation,
parseActivationCommand,
} from "../group-activation.js";
import { parseSendPolicyCommand } from "../send-policy.js";
import {
buildCommandsMessage,
buildHelpMessage,
buildStatusMessage,
formatContextUsageShort,
formatTokenCount,
} from "../status.js";
2026-01-04 05:47:21 +01:00
import type { MsgContext } from "../templating.js";
import type {
ElevatedLevel,
ReasoningLevel,
ThinkLevel,
VerboseLevel,
} from "../thinking.js";
2026-01-04 05:47:21 +01:00
import type { ReplyPayload } from "../types.js";
import { isAbortTrigger, setAbortMemory } from "./abort.js";
2026-01-10 03:00:24 +01:00
import { parseConfigCommand } from "./config-commands.js";
2026-01-09 16:38:52 +01:00
import { parseDebugCommand } from "./debug-commands.js";
2026-01-05 01:31:36 +01:00
import type { InlineDirectives } from "./directive-handling.js";
import { stripMentions, stripStructuralPrefixes } from "./mentions.js";
2026-01-07 07:21:56 +01:00
import { getFollowupQueueDepth, resolveQueueSettings } from "./queue.js";
import { incrementCompactionCount } from "./session-updates.js";
2026-01-04 05:47:21 +01:00
function resolveSessionEntryForKey(
store: Record<string, SessionEntry> | undefined,
sessionKey: string | undefined,
) {
if (!store || !sessionKey) return {};
const direct = store[sessionKey];
if (direct) return { entry: direct, key: sessionKey };
const parsed = parseAgentSessionKey(sessionKey);
const legacyKey = parsed?.rest;
if (legacyKey && store[legacyKey]) {
return { entry: store[legacyKey], key: legacyKey };
}
return {};
}
2026-01-04 05:47:21 +01:00
export type CommandContext = {
surface: string;
provider: string;
isWhatsAppProvider: boolean;
2026-01-04 05:47:21 +01:00
ownerList: string[];
2026-01-05 01:31:36 +01:00
isAuthorizedSender: boolean;
2026-01-04 05:47:21 +01:00
senderE164?: string;
abortKey?: string;
rawBodyNormalized: string;
commandBodyNormalized: string;
from?: string;
to?: string;
};
export async function buildStatusReply(params: {
cfg: ClawdbotConfig;
command: CommandContext;
sessionEntry?: SessionEntry;
sessionKey: string;
sessionScope?: SessionScope;
provider: string;
model: string;
contextTokens: number;
resolvedThinkLevel?: ThinkLevel;
resolvedVerboseLevel: VerboseLevel;
resolvedReasoningLevel: ReasoningLevel;
resolvedElevatedLevel?: ElevatedLevel;
resolveDefaultThinkingLevel: () => Promise<ThinkLevel | undefined>;
isGroup: boolean;
defaultGroupActivation: () => "always" | "mention";
}): Promise<ReplyPayload | undefined> {
const {
cfg,
command,
sessionEntry,
sessionKey,
sessionScope,
provider,
model,
contextTokens,
resolvedThinkLevel,
resolvedVerboseLevel,
resolvedReasoningLevel,
resolvedElevatedLevel,
resolveDefaultThinkingLevel,
isGroup,
defaultGroupActivation,
} = params;
if (!command.isAuthorizedSender) {
logVerbose(
`Ignoring /status from unauthorized sender: ${command.senderE164 || "<unknown>"}`,
);
return undefined;
}
const statusAgentId = sessionKey
? resolveSessionAgentId({ sessionKey, config: cfg })
: resolveDefaultAgentId(cfg);
const statusAgentDir = resolveAgentDir(cfg, statusAgentId);
let usageLine: string | null = null;
try {
const usageProvider = resolveUsageProviderId(provider);
if (usageProvider) {
const usageSummary = await loadProviderUsageSummary({
timeoutMs: 3500,
providers: [usageProvider],
agentDir: statusAgentDir,
});
usageLine = formatUsageSummaryLine(usageSummary, { now: Date.now() });
if (
!usageLine &&
(resolvedVerboseLevel === "on" || resolvedElevatedLevel === "on")
) {
const entry = usageSummary.providers[0];
if (entry?.error) {
usageLine = `📊 Usage: ${entry.displayName} (${entry.error})`;
}
}
}
} catch {
usageLine = null;
}
const queueSettings = resolveQueueSettings({
cfg,
provider: command.provider,
sessionEntry,
});
const queueKey = sessionKey ?? sessionEntry?.sessionId;
const queueDepth = queueKey ? getFollowupQueueDepth(queueKey) : 0;
const queueOverrides = Boolean(
sessionEntry?.queueDebounceMs ??
sessionEntry?.queueCap ??
sessionEntry?.queueDrop,
);
const groupActivation = isGroup
? (normalizeGroupActivation(sessionEntry?.groupActivation) ??
defaultGroupActivation())
: undefined;
const agentDefaults = cfg.agents?.defaults ?? {};
const statusText = buildStatusMessage({
2026-01-09 03:14:39 +00:00
config: cfg,
agent: {
...agentDefaults,
model: {
...agentDefaults.model,
primary: `${provider}/${model}`,
},
contextTokens,
thinkingDefault: agentDefaults.thinkingDefault,
verboseDefault: agentDefaults.verboseDefault,
elevatedDefault: agentDefaults.elevatedDefault,
},
sessionEntry,
sessionKey,
sessionScope,
groupActivation,
resolvedThink: resolvedThinkLevel ?? (await resolveDefaultThinkingLevel()),
resolvedVerbose: resolvedVerboseLevel,
resolvedReasoning: resolvedReasoningLevel,
resolvedElevated: resolvedElevatedLevel,
modelAuth: resolveModelAuthLabel(
provider,
cfg,
sessionEntry,
statusAgentDir,
),
usageLine: usageLine ?? undefined,
queue: {
mode: queueSettings.mode,
depth: queueDepth,
debounceMs: queueSettings.debounceMs,
cap: queueSettings.cap,
dropPolicy: queueSettings.dropPolicy,
showDetails: queueOverrides,
},
includeTranscriptUsage: false,
});
return { text: statusText };
}
2026-01-09 03:38:33 +01:00
function formatApiKeySnippet(apiKey: string): string {
const compact = apiKey.replace(/\s+/g, "");
if (!compact) return "unknown";
const edge = compact.length >= 12 ? 6 : 4;
const head = compact.slice(0, edge);
const tail = compact.slice(-edge);
return `${head}${tail}`;
}
function resolveModelAuthLabel(
provider?: string,
cfg?: ClawdbotConfig,
2026-01-09 03:38:33 +01:00
sessionEntry?: SessionEntry,
agentDir?: string,
): string | undefined {
2026-01-05 15:50:14 +01:00
const resolved = provider?.trim();
if (!resolved) return undefined;
2026-01-09 03:38:33 +01:00
const providerKey = normalizeProviderId(resolved);
const store = ensureAuthProfileStore(agentDir, {
allowKeychainPrompt: false,
});
2026-01-09 03:38:33 +01:00
const profileOverride = sessionEntry?.authProfileOverride?.trim();
const order = resolveAuthProfileOrder({
cfg,
store,
provider: providerKey,
preferredProfile: profileOverride,
});
const candidates = [profileOverride, ...order].filter(Boolean) as string[];
2026-01-09 03:38:33 +01:00
for (const profileId of candidates) {
const profile = store.profiles[profileId];
if (!profile || normalizeProviderId(profile.provider) !== providerKey) {
continue;
}
const label = resolveAuthProfileDisplayLabel({ cfg, store, profileId });
if (profile.type === "oauth") {
return `oauth${label ? ` (${label})` : ""}`;
}
2026-01-09 07:51:47 +01:00
if (profile.type === "token") {
const snippet = formatApiKeySnippet(profile.token);
return `token ${snippet}${label ? ` (${label})` : ""}`;
}
2026-01-09 03:38:33 +01:00
const snippet = formatApiKeySnippet(profile.key);
return `api-key ${snippet}${label ? ` (${label})` : ""}`;
2026-01-05 15:50:14 +01:00
}
2026-01-09 03:38:33 +01:00
const envKey = resolveEnvApiKey(providerKey);
if (envKey?.apiKey) {
2026-01-09 03:38:33 +01:00
if (envKey.source.includes("OAUTH_TOKEN")) {
return `oauth (${envKey.source})`;
}
return `api-key ${formatApiKeySnippet(envKey.apiKey)} (${envKey.source})`;
2026-01-05 15:50:14 +01:00
}
2026-01-09 03:38:33 +01:00
const customKey = getCustomProviderApiKey(cfg, providerKey);
if (customKey) {
return `api-key ${formatApiKeySnippet(customKey)} (models.json)`;
}
2026-01-05 15:50:14 +01:00
return "unknown";
}
function extractCompactInstructions(params: {
rawBody?: string;
ctx: MsgContext;
cfg: ClawdbotConfig;
2026-01-08 22:57:08 +01:00
agentId?: string;
isGroup: boolean;
}): string | undefined {
const raw = stripStructuralPrefixes(params.rawBody ?? "");
const stripped = params.isGroup
2026-01-08 22:57:08 +01:00
? 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;
}
2026-01-04 05:47:21 +01:00
export function buildCommandContext(params: {
ctx: MsgContext;
2026-01-04 14:32:47 +00:00
cfg: ClawdbotConfig;
2026-01-08 22:57:08 +01:00
agentId?: string;
2026-01-04 05:47:21 +01:00
sessionKey?: string;
isGroup: boolean;
triggerBodyNormalized: string;
2026-01-05 01:31:36 +01:00
commandAuthorized: boolean;
2026-01-04 05:47:21 +01:00
}): CommandContext {
2026-01-08 22:57:08 +01:00
const { ctx, cfg, agentId, sessionKey, isGroup, triggerBodyNormalized } =
params;
const auth = resolveCommandAuthorization({
2026-01-05 01:31:36 +01:00
ctx,
cfg,
commandAuthorized: params.commandAuthorized,
});
const surface = (ctx.Surface ?? ctx.Provider ?? "").trim().toLowerCase();
const provider = (ctx.Provider ?? surface).trim().toLowerCase();
const abortKey =
sessionKey ?? (auth.from || undefined) ?? (auth.to || undefined);
2026-01-04 05:47:21 +01:00
const rawBodyNormalized = triggerBodyNormalized;
2026-01-08 03:22:14 +01:00
const commandBodyNormalized = normalizeCommandBody(
2026-01-08 22:57:08 +01:00
isGroup
? stripMentions(rawBodyNormalized, ctx, cfg, agentId)
: rawBodyNormalized,
2026-01-08 03:22:14 +01:00
);
2026-01-04 05:47:21 +01:00
return {
surface,
provider,
isWhatsAppProvider: auth.isWhatsAppProvider,
ownerList: auth.ownerList,
isAuthorizedSender: auth.isAuthorizedSender,
senderE164: auth.senderE164,
2026-01-04 05:47:21 +01:00
abortKey,
rawBodyNormalized,
commandBodyNormalized,
from: auth.from,
to: auth.to,
2026-01-04 05:47:21 +01:00
};
}
function resolveAbortTarget(params: {
ctx: MsgContext;
sessionKey?: string;
sessionEntry?: SessionEntry;
sessionStore?: Record<string, SessionEntry>;
}) {
const targetSessionKey =
params.ctx.CommandTargetSessionKey?.trim() || params.sessionKey;
const { entry, key } = resolveSessionEntryForKey(
params.sessionStore,
targetSessionKey,
);
if (entry && key) return { entry, key, sessionId: entry.sessionId };
if (params.sessionEntry && params.sessionKey) {
return {
entry: params.sessionEntry,
key: params.sessionKey,
sessionId: params.sessionEntry.sessionId,
};
}
return { entry: undefined, key: targetSessionKey, sessionId: undefined };
}
2026-01-04 05:47:21 +01:00
export async function handleCommands(params: {
ctx: MsgContext;
2026-01-04 14:32:47 +00:00
cfg: ClawdbotConfig;
2026-01-04 05:47:21 +01:00
command: CommandContext;
2026-01-08 22:57:08 +01:00
agentId?: string;
2026-01-05 01:31:36 +01:00
directives: InlineDirectives;
2026-01-04 05:47:21 +01:00
sessionEntry?: SessionEntry;
sessionStore?: Record<string, SessionEntry>;
sessionKey: string;
2026-01-04 05:47:21 +01:00
storePath?: string;
2026-01-04 07:05:04 +01:00
sessionScope?: SessionScope;
2026-01-04 05:47:21 +01:00
workspaceDir: string;
defaultGroupActivation: () => "always" | "mention";
resolvedThinkLevel?: ThinkLevel;
resolvedVerboseLevel: VerboseLevel;
resolvedReasoningLevel: ReasoningLevel;
2026-01-05 07:07:17 +01:00
resolvedElevatedLevel?: ElevatedLevel;
2026-01-04 05:47:21 +01:00
resolveDefaultThinkingLevel: () => Promise<ThinkLevel | undefined>;
provider: string;
model: string;
contextTokens: number;
isGroup: boolean;
}): Promise<{
reply?: ReplyPayload;
shouldContinue: boolean;
}> {
const {
ctx,
2026-01-04 05:47:21 +01:00
cfg,
command,
2026-01-05 01:31:36 +01:00
directives,
2026-01-04 05:47:21 +01:00
sessionEntry,
sessionStore,
sessionKey,
storePath,
sessionScope,
workspaceDir,
defaultGroupActivation,
resolvedThinkLevel,
resolvedVerboseLevel,
resolvedReasoningLevel,
2026-01-05 07:07:17 +01:00
resolvedElevatedLevel,
2026-01-04 05:47:21 +01:00
resolveDefaultThinkingLevel,
2026-01-05 17:15:17 +00:00
provider,
2026-01-04 05:47:21 +01:00
model,
contextTokens,
isGroup,
} = params;
const resetRequested =
command.commandBodyNormalized === "/reset" ||
command.commandBodyNormalized === "/new";
if (resetRequested && !command.isAuthorizedSender) {
logVerbose(
`Ignoring /reset from unauthorized sender: ${command.senderE164 || "<unknown>"}`,
);
return { shouldContinue: false };
}
2026-01-04 05:47:21 +01:00
const activationCommand = parseActivationCommand(
command.commandBodyNormalized,
);
const sendPolicyCommand = parseSendPolicyCommand(
command.commandBodyNormalized,
);
const allowTextCommands = shouldHandleTextCommands({
cfg,
surface: command.surface,
commandSource: ctx.CommandSource,
});
2026-01-04 05:47:21 +01:00
if (allowTextCommands && activationCommand.hasCommand) {
2026-01-04 05:47:21 +01:00
if (!isGroup) {
return {
shouldContinue: false,
reply: { text: "⚙️ Group activation only applies to group chats." },
};
}
const activationOwnerList = command.ownerList;
const activationSenderE164 = command.senderE164
? normalizeE164(command.senderE164)
: "";
const isActivationOwner =
!command.isWhatsAppProvider || activationOwnerList.length === 0
? command.isAuthorizedSender
: Boolean(activationSenderE164) &&
activationOwnerList.includes(activationSenderE164);
if (
!command.isAuthorizedSender ||
(command.isWhatsAppProvider && !isActivationOwner)
) {
2026-01-04 05:47:21 +01:00
logVerbose(
2026-01-05 01:31:36 +01:00
`Ignoring /activation from unauthorized sender in group: ${command.senderE164 || "<unknown>"}`,
2026-01-04 05:47:21 +01:00
);
return { shouldContinue: false };
}
if (!activationCommand.mode) {
return {
shouldContinue: false,
reply: { text: "⚙️ Usage: /activation mention|always" },
};
}
if (sessionEntry && sessionStore && sessionKey) {
sessionEntry.groupActivation = activationCommand.mode;
sessionEntry.groupActivationNeedsSystemIntro = true;
sessionEntry.updatedAt = Date.now();
sessionStore[sessionKey] = sessionEntry;
if (storePath) {
await saveSessionStore(storePath, sessionStore);
}
}
return {
shouldContinue: false,
reply: { text: `⚙️ Group activation set to ${activationCommand.mode}.` },
};
}
if (allowTextCommands && sendPolicyCommand.hasCommand) {
2026-01-05 01:31:36 +01:00
if (!command.isAuthorizedSender) {
2026-01-04 05:47:21 +01:00
logVerbose(
2026-01-05 01:31:36 +01:00
`Ignoring /send from unauthorized sender: ${command.senderE164 || "<unknown>"}`,
2026-01-04 05:47:21 +01:00
);
return { shouldContinue: false };
}
if (!sendPolicyCommand.mode) {
return {
shouldContinue: false,
reply: { text: "⚙️ Usage: /send on|off|inherit" },
};
}
if (sessionEntry && sessionStore && sessionKey) {
if (sendPolicyCommand.mode === "inherit") {
delete sessionEntry.sendPolicy;
} else {
sessionEntry.sendPolicy = sendPolicyCommand.mode;
}
sessionEntry.updatedAt = Date.now();
sessionStore[sessionKey] = sessionEntry;
if (storePath) {
await saveSessionStore(storePath, sessionStore);
}
}
const label =
sendPolicyCommand.mode === "inherit"
? "inherit"
: sendPolicyCommand.mode === "allow"
? "on"
: "off";
return {
shouldContinue: false,
reply: { text: `⚙️ Send policy set to ${label}.` },
};
}
if (allowTextCommands && command.commandBodyNormalized === "/restart") {
2026-01-05 01:31:36 +01:00
if (!command.isAuthorizedSender) {
2026-01-04 05:47:21 +01:00
logVerbose(
2026-01-05 01:31:36 +01:00
`Ignoring /restart from unauthorized sender: ${command.senderE164 || "<unknown>"}`,
2026-01-04 05:47:21 +01:00
);
return { shouldContinue: false };
}
2026-01-09 05:49:11 +00:00
if (cfg.commands?.restart !== true) {
return {
shouldContinue: false,
reply: {
text: "⚠️ /restart is disabled. Set commands.restart=true to enable.",
},
};
}
2026-01-08 06:48:28 +00:00
const hasSigusr1Listener = process.listenerCount("SIGUSR1") > 0;
if (hasSigusr1Listener) {
scheduleGatewaySigusr1Restart({ reason: "/restart" });
return {
shouldContinue: false,
reply: {
text: "⚙️ Restarting clawdbot in-process (SIGUSR1); back in a few seconds.",
},
};
}
2026-01-04 14:32:47 +00:00
const restartMethod = triggerClawdbotRestart();
2026-01-08 06:48:28 +00:00
if (!restartMethod.ok) {
const detail = restartMethod.detail
? ` Details: ${restartMethod.detail}`
: "";
return {
shouldContinue: false,
reply: {
text: `⚠️ Restart failed (${restartMethod.method}).${detail}`,
},
};
}
2026-01-04 05:47:21 +01:00
return {
shouldContinue: false,
reply: {
2026-01-08 06:48:28 +00:00
text: `⚙️ Restarting clawdbot via ${restartMethod.method}; give me a few seconds to come back online.`,
2026-01-04 05:47:21 +01:00
},
};
}
const helpRequested = command.commandBodyNormalized === "/help";
if (allowTextCommands && helpRequested) {
2026-01-05 07:07:17 +01:00
if (!command.isAuthorizedSender) {
logVerbose(
`Ignoring /help from unauthorized sender: ${command.senderE164 || "<unknown>"}`,
);
return { shouldContinue: false };
}
return { shouldContinue: false, reply: { text: buildHelpMessage() } };
}
const commandsRequested = command.commandBodyNormalized === "/commands";
if (allowTextCommands && commandsRequested) {
if (!command.isAuthorizedSender) {
logVerbose(
`Ignoring /commands from unauthorized sender: ${command.senderE164 || "<unknown>"}`,
);
return { shouldContinue: false };
}
return { shouldContinue: false, reply: { text: buildCommandsMessage() } };
}
2026-01-05 01:31:36 +01:00
const statusRequested =
directives.hasStatusDirective ||
command.commandBodyNormalized === "/status";
if (allowTextCommands && statusRequested) {
const reply = await buildStatusReply({
2026-01-07 07:21:56 +01:00
cfg,
command,
2026-01-04 05:47:21 +01:00
sessionEntry,
sessionKey,
sessionScope,
provider,
model,
contextTokens,
resolvedThinkLevel,
resolvedVerboseLevel,
resolvedReasoningLevel,
resolvedElevatedLevel,
resolveDefaultThinkingLevel,
isGroup,
defaultGroupActivation,
2026-01-04 05:47:21 +01:00
});
return { shouldContinue: false, reply };
2026-01-04 05:47:21 +01:00
}
2026-01-10 03:00:24 +01:00
const configCommand = allowTextCommands
? parseConfigCommand(command.commandBodyNormalized)
: null;
if (configCommand) {
if (!command.isAuthorizedSender) {
logVerbose(
`Ignoring /config from unauthorized sender: ${command.senderE164 || "<unknown>"}`,
);
return { shouldContinue: false };
}
if (configCommand.action === "error") {
return {
shouldContinue: false,
reply: { text: `⚠️ ${configCommand.message}` },
};
}
const snapshot = await readConfigFileSnapshot();
2026-01-10 02:11:43 +00:00
if (
!snapshot.valid ||
!snapshot.parsed ||
typeof snapshot.parsed !== "object"
) {
2026-01-10 03:00:24 +01:00
return {
shouldContinue: false,
2026-01-10 02:11:43 +00:00
reply: {
text: "⚠️ Config file is invalid; fix it before using /config.",
},
2026-01-10 03:00:24 +01:00
};
}
const parsedBase = structuredClone(
snapshot.parsed as Record<string, unknown>,
);
if (configCommand.action === "show") {
const pathRaw = configCommand.path?.trim();
if (pathRaw) {
const parsedPath = parseConfigPath(pathRaw);
if (!parsedPath.ok || !parsedPath.path) {
return {
shouldContinue: false,
reply: { text: `⚠️ ${parsedPath.error ?? "Invalid path."}` },
};
}
const value = getConfigValueAtPath(parsedBase, parsedPath.path);
const rendered = JSON.stringify(value ?? null, null, 2);
return {
shouldContinue: false,
reply: {
text: `⚙️ Config ${pathRaw}:\n\`\`\`json\n${rendered}\n\`\`\``,
},
};
}
const json = JSON.stringify(parsedBase, null, 2);
return {
shouldContinue: false,
reply: { text: `⚙️ Config (raw):\n\`\`\`json\n${json}\n\`\`\`` },
};
}
if (configCommand.action === "unset") {
const parsedPath = parseConfigPath(configCommand.path);
if (!parsedPath.ok || !parsedPath.path) {
return {
shouldContinue: false,
reply: { text: `⚠️ ${parsedPath.error ?? "Invalid path."}` },
};
}
const removed = unsetConfigValueAtPath(parsedBase, parsedPath.path);
if (!removed) {
return {
shouldContinue: false,
reply: { text: `⚙️ No config value found for ${configCommand.path}.` },
};
}
const validated = validateConfigObject(parsedBase);
if (!validated.ok) {
const issue = validated.issues[0];
return {
shouldContinue: false,
reply: {
text: `⚠️ Config invalid after unset (${issue.path}: ${issue.message}).`,
},
};
}
await writeConfigFile(validated.config);
return {
shouldContinue: false,
reply: { text: `⚙️ Config updated: ${configCommand.path} removed.` },
};
}
if (configCommand.action === "set") {
const parsedPath = parseConfigPath(configCommand.path);
if (!parsedPath.ok || !parsedPath.path) {
return {
shouldContinue: false,
reply: { text: `⚠️ ${parsedPath.error ?? "Invalid path."}` },
};
}
setConfigValueAtPath(parsedBase, parsedPath.path, configCommand.value);
const validated = validateConfigObject(parsedBase);
if (!validated.ok) {
const issue = validated.issues[0];
return {
shouldContinue: false,
reply: {
text: `⚠️ Config invalid after set (${issue.path}: ${issue.message}).`,
},
};
}
await writeConfigFile(validated.config);
const valueLabel =
typeof configCommand.value === "string"
? `"${configCommand.value}"`
: JSON.stringify(configCommand.value);
return {
shouldContinue: false,
reply: {
text: `⚙️ Config updated: ${configCommand.path}=${valueLabel ?? "null"}`,
},
};
}
}
2026-01-09 16:38:52 +01:00
const debugCommand = allowTextCommands
? parseDebugCommand(command.commandBodyNormalized)
: null;
if (debugCommand) {
if (!command.isAuthorizedSender) {
logVerbose(
`Ignoring /debug from unauthorized sender: ${command.senderE164 || "<unknown>"}`,
);
return { shouldContinue: false };
}
if (debugCommand.action === "error") {
2026-01-09 16:55:51 +01:00
return {
shouldContinue: false,
reply: { text: `⚠️ ${debugCommand.message}` },
};
2026-01-09 16:38:52 +01:00
}
if (debugCommand.action === "show") {
const overrides = getConfigOverrides();
const hasOverrides = Object.keys(overrides).length > 0;
if (!hasOverrides) {
return {
shouldContinue: false,
reply: { text: "⚙️ Debug overrides: (none)" },
};
}
2026-01-10 03:10:01 +01:00
const effectiveConfig = cfg ?? {};
2026-01-09 16:38:52 +01:00
const json = JSON.stringify(overrides, null, 2);
2026-01-10 03:10:01 +01:00
const effectiveJson = JSON.stringify(effectiveConfig, null, 2);
2026-01-09 16:38:52 +01:00
return {
shouldContinue: false,
2026-01-09 16:55:51 +01:00
reply: {
2026-01-10 03:10:01 +01:00
text: `⚙️ Debug overrides (memory-only):\n\`\`\`json\n${json}\n\`\`\`\n⚙ Effective config (with overrides):\n\`\`\`json\n${effectiveJson}\n\`\`\``,
2026-01-09 16:55:51 +01:00
},
2026-01-09 16:38:52 +01:00
};
}
if (debugCommand.action === "reset") {
resetConfigOverrides();
return {
shouldContinue: false,
reply: { text: "⚙️ Debug overrides cleared; using config on disk." },
};
}
if (debugCommand.action === "unset") {
const result = unsetConfigOverride(debugCommand.path);
if (!result.ok) {
return {
shouldContinue: false,
reply: { text: `⚠️ ${result.error ?? "Invalid path."}` },
};
}
if (!result.removed) {
return {
shouldContinue: false,
2026-01-09 16:55:51 +01:00
reply: {
text: `⚙️ No debug override found for ${debugCommand.path}.`,
},
2026-01-09 16:38:52 +01:00
};
}
return {
shouldContinue: false,
reply: { text: `⚙️ Debug override removed for ${debugCommand.path}.` },
};
}
if (debugCommand.action === "set") {
const result = setConfigOverride(debugCommand.path, debugCommand.value);
if (!result.ok) {
return {
shouldContinue: false,
reply: { text: `⚠️ ${result.error ?? "Invalid override."}` },
};
}
const valueLabel =
typeof debugCommand.value === "string"
? `"${debugCommand.value}"`
: JSON.stringify(debugCommand.value);
return {
shouldContinue: false,
reply: {
text: `⚙️ Debug override set: ${debugCommand.path}=${valueLabel ?? "null"}`,
},
};
}
}
const stopRequested = command.commandBodyNormalized === "/stop";
if (allowTextCommands && stopRequested) {
if (!command.isAuthorizedSender) {
logVerbose(
`Ignoring /stop from unauthorized sender: ${command.senderE164 || "<unknown>"}`,
);
return { shouldContinue: false };
}
const abortTarget = resolveAbortTarget({
ctx,
sessionKey,
sessionEntry,
sessionStore,
});
if (abortTarget.sessionId) {
abortEmbeddedPiRun(abortTarget.sessionId);
}
if (abortTarget.entry && sessionStore && abortTarget.key) {
abortTarget.entry.abortedLastRun = true;
abortTarget.entry.updatedAt = Date.now();
sessionStore[abortTarget.key] = abortTarget.entry;
if (storePath) {
await saveSessionStore(storePath, sessionStore);
}
} else if (command.abortKey) {
setAbortMemory(command.abortKey, true);
}
return { shouldContinue: false, reply: { text: "⚙️ Agent was aborted." } };
}
const compactRequested =
command.commandBodyNormalized === "/compact" ||
command.commandBodyNormalized.startsWith("/compact ");
if (compactRequested) {
if (!command.isAuthorizedSender) {
logVerbose(
`Ignoring /compact from unauthorized sender: ${command.senderE164 || "<unknown>"}`,
);
return { shouldContinue: false };
}
if (!sessionEntry?.sessionId) {
return {
shouldContinue: false,
reply: { text: "⚙️ Compaction unavailable (missing session id)." },
};
}
const sessionId = sessionEntry.sessionId;
if (isEmbeddedPiRunActive(sessionId)) {
abortEmbeddedPiRun(sessionId);
await waitForEmbeddedPiRunEnd(sessionId, 15_000);
}
const customInstructions = extractCompactInstructions({
rawBody: ctx.Body,
ctx,
cfg,
2026-01-08 22:57:08 +01:00
agentId: params.agentId,
isGroup,
});
const result = await compactEmbeddedPiSession({
sessionId,
sessionKey,
messageProvider: command.provider,
sessionFile: resolveSessionFilePath(sessionId, sessionEntry),
workspaceDir,
config: cfg,
skillsSnapshot: sessionEntry.skillsSnapshot,
provider,
model,
thinkLevel: resolvedThinkLevel ?? (await resolveDefaultThinkingLevel()),
bashElevated: {
enabled: false,
allowed: false,
defaultLevel: "off",
},
customInstructions,
ownerNumbers:
command.ownerList.length > 0 ? command.ownerList : undefined,
});
const totalTokens =
sessionEntry.totalTokens ??
(sessionEntry.inputTokens ?? 0) + (sessionEntry.outputTokens ?? 0);
const contextSummary = formatContextUsageShort(
totalTokens > 0 ? totalTokens : null,
contextTokens ?? sessionEntry.contextTokens ?? null,
);
const compactLabel = result.ok
? result.compacted
? result.result?.tokensBefore
? `Compacted (${formatTokenCount(result.result.tokensBefore)} before)`
: "Compacted"
: "Compaction skipped"
: "Compaction failed";
if (result.ok && result.compacted) {
await incrementCompactionCount({
sessionEntry,
sessionStore,
sessionKey,
storePath,
});
}
const reason = result.reason?.trim();
const line = reason
? `${compactLabel}: ${reason}${contextSummary}`
: `${compactLabel}${contextSummary}`;
enqueueSystemEvent(line, { sessionKey });
return { shouldContinue: false, reply: { text: `⚙️ ${line}` } };
}
2026-01-04 05:47:21 +01:00
const abortRequested = isAbortTrigger(command.rawBodyNormalized);
if (allowTextCommands && abortRequested) {
const abortTarget = resolveAbortTarget({
ctx,
sessionKey,
sessionEntry,
sessionStore,
});
if (abortTarget.sessionId) {
abortEmbeddedPiRun(abortTarget.sessionId);
}
if (abortTarget.entry && sessionStore && abortTarget.key) {
abortTarget.entry.abortedLastRun = true;
abortTarget.entry.updatedAt = Date.now();
sessionStore[abortTarget.key] = abortTarget.entry;
2026-01-04 05:47:21 +01:00
if (storePath) {
await saveSessionStore(storePath, sessionStore);
}
} else if (command.abortKey) {
setAbortMemory(command.abortKey, true);
}
return { shouldContinue: false, reply: { text: "⚙️ Agent was aborted." } };
}
const sendPolicy = resolveSendPolicy({
cfg,
entry: sessionEntry,
sessionKey,
provider: sessionEntry?.provider ?? command.provider,
2026-01-04 05:47:21 +01:00
chatType: sessionEntry?.chatType,
});
if (sendPolicy === "deny") {
logVerbose(`Send blocked by policy for session ${sessionKey ?? "unknown"}`);
return { shouldContinue: false };
}
return { shouldContinue: true };
}