chore: Enable "curly" rule to avoid single-statement if confusion/errors.

This commit is contained in:
cpojer
2026-01-31 16:19:20 +09:00
parent 009b16fab8
commit 5ceff756e1
1266 changed files with 27871 additions and 9393 deletions

View File

@@ -24,7 +24,9 @@ const ABORT_TRIGGERS = new Set(["stop", "esc", "abort", "wait", "exit", "interru
const ABORT_MEMORY = new Map<string, boolean>();
export function isAbortTrigger(text?: string): boolean {
if (!text) return false;
if (!text) {
return false;
}
const normalized = text.trim().toLowerCase();
return ABORT_TRIGGERS.has(normalized);
}
@@ -49,15 +51,21 @@ function resolveSessionEntryForKey(
store: Record<string, SessionEntry> | undefined,
sessionKey: string | undefined,
) {
if (!store || !sessionKey) return {};
if (!store || !sessionKey) {
return {};
}
const direct = store[sessionKey];
if (direct) return { entry: direct, key: sessionKey };
if (direct) {
return { entry: direct, key: sessionKey };
}
return {};
}
function resolveAbortTargetKey(ctx: MsgContext): string | undefined {
const target = ctx.CommandTargetSessionKey?.trim();
if (target) return target;
if (target) {
return target;
}
const sessionKey = ctx.SessionKey?.trim();
return sessionKey || undefined;
}
@@ -67,7 +75,9 @@ function normalizeRequesterSessionKey(
key: string | undefined,
): string | undefined {
const cleaned = key?.trim();
if (!cleaned) return undefined;
if (!cleaned) {
return undefined;
}
const { mainKey, alias } = resolveMainSessionAlias(cfg);
return resolveInternalSessionKey({ key: cleaned, alias, mainKey });
}
@@ -77,18 +87,26 @@ export function stopSubagentsForRequester(params: {
requesterSessionKey?: string;
}): { stopped: number } {
const requesterKey = normalizeRequesterSessionKey(params.cfg, params.requesterSessionKey);
if (!requesterKey) return { stopped: 0 };
if (!requesterKey) {
return { stopped: 0 };
}
const runs = listSubagentRunsForRequester(requesterKey);
if (runs.length === 0) return { stopped: 0 };
if (runs.length === 0) {
return { stopped: 0 };
}
const storeCache = new Map<string, Record<string, SessionEntry>>();
const seenChildKeys = new Set<string>();
let stopped = 0;
for (const run of runs) {
if (run.endedAt) continue;
if (run.endedAt) {
continue;
}
const childKey = run.childSessionKey?.trim();
if (!childKey || seenChildKeys.has(childKey)) continue;
if (!childKey || seenChildKeys.has(childKey)) {
continue;
}
seenChildKeys.add(childKey);
const cleared = clearSessionQueues([childKey]);
@@ -130,7 +148,9 @@ export async function tryFastAbortFromMessage(params: {
const stripped = isGroup ? stripMentions(raw, ctx, cfg, agentId) : raw;
const normalized = normalizeCommandBody(stripped);
const abortRequested = normalized === "/stop" || isAbortTrigger(stripped);
if (!abortRequested) return { handled: false, aborted: false };
if (!abortRequested) {
return { handled: false, aborted: false };
}
const commandAuthorized = ctx.CommandAuthorized;
const auth = resolveCommandAuthorization({
@@ -138,7 +158,9 @@ export async function tryFastAbortFromMessage(params: {
cfg,
commandAuthorized,
});
if (!auth.isAuthorizedSender) return { handled: false, aborted: false };
if (!auth.isAuthorizedSender) {
return { handled: false, aborted: false };
}
const abortKey = targetKey ?? auth.from ?? auth.to;
const requesterSessionKey = targetKey ?? ctx.SessionKey ?? abortKey;
@@ -161,7 +183,9 @@ export async function tryFastAbortFromMessage(params: {
store[key] = entry;
await updateSessionStore(storePath, (nextStore) => {
const nextEntry = nextStore[key] ?? entry;
if (!nextEntry) return;
if (!nextEntry) {
return;
}
nextEntry.abortedLastRun = true;
nextEntry.updatedAt = Date.now();
nextStore[key] = nextEntry;

View File

@@ -103,7 +103,9 @@ export async function runAgentTurnWithFallback(params: {
params.followupRun.run.reasoningLevel === "stream" && params.opts?.onReasoningStream
);
const normalizeStreamingText = (payload: ReplyPayload): { text?: string; skip: boolean } => {
if (!allowPartialStream) return { skip: true };
if (!allowPartialStream) {
return { skip: true };
}
let text = payload.text;
if (!params.isHeartbeat && text?.includes("HEARTBEAT_OK")) {
const stripped = stripHeartbeatToken(text, {
@@ -121,14 +123,20 @@ export async function runAgentTurnWithFallback(params: {
if (isSilentReplyText(text, SILENT_REPLY_TOKEN)) {
return { skip: true };
}
if (!text) return { skip: true };
if (!text) {
return { skip: true };
}
const sanitized = sanitizeUserFacingText(text);
if (!sanitized.trim()) return { skip: true };
if (!sanitized.trim()) {
return { skip: true };
}
return { text: sanitized, skip: false };
};
const handlePartialForTyping = async (payload: ReplyPayload): Promise<string | undefined> => {
const { text, skip } = normalizeStreamingText(payload);
if (skip || !text) return undefined;
if (skip || !text) {
return undefined;
}
await params.typingSignals.signalTextDelta(text);
return text;
};
@@ -266,7 +274,9 @@ export async function runAgentTurnWithFallback(params: {
params.sessionCtx.Surface,
params.sessionCtx.Provider,
);
if (!channel) return "markdown";
if (!channel) {
return "markdown";
}
return isMarkdownCapableMessageChannel(channel) ? "markdown" : "plain";
})(),
bashElevated: params.followupRun.run.bashElevated,
@@ -279,7 +289,9 @@ export async function runAgentTurnWithFallback(params: {
onPartialReply: allowPartialStream
? async (payload) => {
const textForTyping = await handlePartialForTyping(payload);
if (!params.opts?.onPartialReply || textForTyping === undefined) return;
if (!params.opts?.onPartialReply || textForTyping === undefined) {
return;
}
await params.opts.onPartialReply({
text: textForTyping,
mediaUrls: payload.mediaUrls,
@@ -324,7 +336,9 @@ export async function runAgentTurnWithFallback(params: {
? async (payload) => {
const { text, skip } = normalizeStreamingText(payload);
const hasPayloadMedia = (payload.mediaUrls?.length ?? 0) > 0;
if (skip && !hasPayloadMedia) return;
if (skip && !hasPayloadMedia) {
return;
}
const currentMessageId =
params.sessionCtx.MessageSidFull ?? params.sessionCtx.MessageSid;
const taggedPayload = applyReplyTagsToPayload(
@@ -339,7 +353,9 @@ export async function runAgentTurnWithFallback(params: {
currentMessageId,
);
// Let through payloads with audioAsVoice flag even if empty (need to track it)
if (!isRenderablePayload(taggedPayload) && !payload.audioAsVoice) return;
if (!isRenderablePayload(taggedPayload) && !payload.audioAsVoice) {
return;
}
const parsed = parseReplyDirectives(taggedPayload.text ?? "", {
currentMessageId,
silentToken: SILENT_REPLY_TOKEN,
@@ -353,9 +369,12 @@ export async function runAgentTurnWithFallback(params: {
!hasRenderableMedia &&
!payload.audioAsVoice &&
!parsed.audioAsVoice
)
) {
return;
if (parsed.isSilent && !hasRenderableMedia) return;
}
if (parsed.isSilent && !hasRenderableMedia) {
return;
}
const blockPayload: ReplyPayload = params.applyReplyToMode({
...taggedPayload,
@@ -399,7 +418,9 @@ export async function runAgentTurnWithFallback(params: {
// a typing loop that never sees a matching markRunComplete(). Track and drain.
const task = (async () => {
const { text, skip } = normalizeStreamingText(payload);
if (skip) return;
if (skip) {
return;
}
await params.typingSignals.signalTextDelta(text);
await onToolResult({
text,

View File

@@ -26,7 +26,9 @@ export const createShouldEmitToolResult = (params: {
const store = loadSessionStore(params.storePath);
const entry = store[params.sessionKey];
const current = normalizeVerboseLevel(String(entry?.verboseLevel ?? ""));
if (current) return current !== "off";
if (current) {
return current !== "off";
}
} catch {
// ignore store read failures
}
@@ -49,7 +51,9 @@ export const createShouldEmitToolOutput = (params: {
const store = loadSessionStore(params.storePath);
const entry = store[params.sessionKey];
const current = normalizeVerboseLevel(String(entry?.verboseLevel ?? ""));
if (current) return current === "full";
if (current) {
return current === "full";
}
} catch {
// ignore store read failures
}
@@ -72,9 +76,15 @@ export const signalTypingIfNeeded = async (
): Promise<void> => {
const shouldSignalTyping = payloads.some((payload) => {
const trimmed = payload.text?.trim();
if (trimmed) return true;
if (payload.mediaUrl) return true;
if (payload.mediaUrls && payload.mediaUrls.length > 0) return true;
if (trimmed) {
return true;
}
if (payload.mediaUrl) {
return true;
}
if (payload.mediaUrls && payload.mediaUrls.length > 0) {
return true;
}
return false;
});
if (shouldSignalTyping) {

View File

@@ -39,15 +39,21 @@ export async function runMemoryFlushIfNeeded(params: {
isHeartbeat: boolean;
}): Promise<SessionEntry | undefined> {
const memoryFlushSettings = resolveMemoryFlushSettings(params.cfg);
if (!memoryFlushSettings) return params.sessionEntry;
if (!memoryFlushSettings) {
return params.sessionEntry;
}
const memoryFlushWritable = (() => {
if (!params.sessionKey) return true;
if (!params.sessionKey) {
return true;
}
const runtime = resolveSandboxRuntimeStatus({
cfg: params.cfg,
sessionKey: params.sessionKey,
});
if (!runtime.sandboxed) return true;
if (!runtime.sandboxed) {
return true;
}
const sandboxCfg = resolveSandboxConfigForAgent(params.cfg, runtime.agentId);
return sandboxCfg.workspaceAccess === "rw";
})();
@@ -69,7 +75,9 @@ export async function runMemoryFlushIfNeeded(params: {
softThresholdTokens: memoryFlushSettings.softThresholdTokens,
});
if (!shouldFlushMemory) return params.sessionEntry;
if (!shouldFlushMemory) {
return params.sessionEntry;
}
let activeSessionEntry = params.sessionEntry;
const activeSessionStore = params.sessionStore;

View File

@@ -52,7 +52,9 @@ export function buildReplyPayloads(params: {
logVerbose("Stripped stray HEARTBEAT_OK token from reply");
}
const hasMedia = Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0;
if (stripped.shouldSkip && !hasMedia) return [];
if (stripped.shouldSkip && !hasMedia) {
return [];
}
return [{ ...payload, text: stripped.text }];
});

View File

@@ -20,9 +20,13 @@ export function buildThreadingToolContext(params: {
hasRepliedRef: { value: boolean } | undefined;
}): ChannelThreadingToolContext {
const { sessionCtx, config, hasRepliedRef } = params;
if (!config) return {};
if (!config) {
return {};
}
const rawProvider = sessionCtx.Provider?.trim().toLowerCase();
if (!rawProvider) return {};
if (!rawProvider) {
return {};
}
const provider = normalizeChannelId(rawProvider) ?? normalizeAnyChannelId(rawProvider);
// Fallback for unrecognized/plugin channels (e.g., BlueBubbles before plugin registry init)
const dock = provider ? getChannelDock(provider) : undefined;
@@ -78,10 +82,14 @@ export const formatResponseUsageLine = (params: {
};
}): string | null => {
const usage = params.usage;
if (!usage) return null;
if (!usage) {
return null;
}
const input = usage.input;
const output = usage.output;
if (typeof input !== "number" && typeof output !== "number") return null;
if (typeof input !== "number" && typeof output !== "number") {
return null;
}
const inputLabel = typeof input === "number" ? formatTokenCount(input) : "?";
const outputLabel = typeof output === "number" ? formatTokenCount(output) : "?";
const cost =
@@ -109,7 +117,9 @@ export const appendUsageLine = (payloads: ReplyPayload[], line: string): ReplyPa
break;
}
}
if (index === -1) return [...payloads, { text: line }];
if (index === -1) {
return [...payloads, { text: line }];
}
const existing = payloads[index];
const existingText = existing.text ?? "";
const separator = existingText.endsWith("\n") ? "" : "\n";

View File

@@ -106,10 +106,16 @@ describe("runReplyAgent claude-cli routing", () => {
const randomSpy = vi.spyOn(crypto, "randomUUID").mockReturnValue("run-1");
const lifecyclePhases: string[] = [];
const unsubscribe = onAgentEvent((evt) => {
if (evt.runId !== "run-1") return;
if (evt.stream !== "lifecycle") return;
if (evt.runId !== "run-1") {
return;
}
if (evt.stream !== "lifecycle") {
return;
}
const phase = evt.data?.phase;
if (typeof phase === "string") lifecyclePhases.push(phase);
if (typeof phase === "string") {
lifecyclePhases.push(phase);
}
});
runCliAgentMock.mockResolvedValueOnce({
payloads: [{ text: "ok" }],

View File

@@ -236,9 +236,13 @@ export async function runReplyAgent(params: {
buildLogMessage,
cleanupTranscripts,
}: SessionResetOptions): Promise<boolean> => {
if (!sessionKey || !activeSessionStore || !storePath) return false;
if (!sessionKey || !activeSessionStore || !storePath) {
return false;
}
const prevEntry = activeSessionStore[sessionKey] ?? activeSessionEntry;
if (!prevEntry) return false;
if (!prevEntry) {
return false;
}
const prevSessionId = cleanupTranscripts ? prevEntry.sessionId : undefined;
const nextSessionId = crypto.randomUUID();
const nextEntry: SessionEntry = {
@@ -273,7 +277,9 @@ export async function runReplyAgent(params: {
if (cleanupTranscripts && prevSessionId) {
const transcriptCandidates = new Set<string>();
const resolved = resolveSessionFilePath(prevSessionId, prevEntry, { agentId });
if (resolved) transcriptCandidates.add(resolved);
if (resolved) {
transcriptCandidates.add(resolved);
}
transcriptCandidates.add(resolveSessionTranscriptPath(prevSessionId, agentId));
for (const candidate of transcriptCandidates) {
try {
@@ -391,8 +397,9 @@ export async function runReplyAgent(params: {
// Drain any late tool/block deliveries before deciding there's "nothing to send".
// Otherwise, a late typing trigger (e.g. from a tool callback) can outlive the run and
// keep the typing indicator stuck.
if (payloadArray.length === 0)
if (payloadArray.length === 0) {
return finalizeWithFollowup(undefined, queueKey, runFollowupTurn);
}
const payloadResult = buildReplyPayloads({
payloads: payloadArray,
@@ -413,8 +420,9 @@ export async function runReplyAgent(params: {
const { replyPayloads } = payloadResult;
didLogHeartbeatStrip = payloadResult.didLogHeartbeatStrip;
if (replyPayloads.length === 0)
if (replyPayloads.length === 0) {
return finalizeWithFollowup(undefined, queueKey, runFollowupTurn);
}
await signalTypingIfNeeded(replyPayloads, typingSignals);
@@ -477,7 +485,9 @@ export async function runReplyAgent(params: {
if (formatted && responseUsageMode === "full" && sessionKey) {
formatted = `${formatted} · session ${sessionKey}`;
}
if (formatted) responseUsageLine = formatted;
if (formatted) {
responseUsageLine = formatted;
}
}
// If verbose is enabled and this is a new session, prepend a session hint.

View File

@@ -35,19 +35,25 @@ let activeJob: ActiveBashJob | null = null;
function resolveForegroundMs(cfg: OpenClawConfig): number {
const raw = cfg.commands?.bashForegroundMs;
if (typeof raw !== "number" || Number.isNaN(raw)) return DEFAULT_FOREGROUND_MS;
if (typeof raw !== "number" || Number.isNaN(raw)) {
return DEFAULT_FOREGROUND_MS;
}
return clampInt(raw, 0, MAX_FOREGROUND_MS);
}
function formatSessionSnippet(sessionId: string) {
const trimmed = sessionId.trim();
if (trimmed.length <= 12) return trimmed;
if (trimmed.length <= 12) {
return trimmed;
}
return `${trimmed.slice(0, 8)}`;
}
function formatOutputBlock(text: string) {
const trimmed = text.trim();
if (!trimmed) return "(no output)";
if (!trimmed) {
return "(no output)";
}
return `\`\`\`txt\n${trimmed}\n\`\`\``;
}
@@ -56,7 +62,9 @@ function parseBashRequest(raw: string): BashRequest | null {
let restSource = "";
if (trimmed.toLowerCase().startsWith("/bash")) {
const match = trimmed.match(/^\/bash(?:\s*:\s*|\s+|$)([\s\S]*)$/i);
if (!match) return null;
if (!match) {
return null;
}
restSource = match[1] ?? "";
} else if (trimmed.startsWith("!")) {
restSource = trimmed.slice(1);
@@ -68,7 +76,9 @@ function parseBashRequest(raw: string): BashRequest | null {
}
const rest = restSource.trimStart();
if (!rest) return { action: "help" };
if (!rest) {
return { action: "help" };
}
const tokenMatch = rest.match(/^(\S+)(?:\s+([\s\S]+))?$/);
const token = tokenMatch?.[1]?.trim() ?? "";
const remainder = tokenMatch?.[2]?.trim() ?? "";
@@ -100,17 +110,27 @@ function resolveRawCommandBody(params: {
function getScopedSession(sessionId: string) {
const running = getSession(sessionId);
if (running && running.scopeKey === CHAT_BASH_SCOPE_KEY) return { running };
if (running && running.scopeKey === CHAT_BASH_SCOPE_KEY) {
return { running };
}
const finished = getFinishedSession(sessionId);
if (finished && finished.scopeKey === CHAT_BASH_SCOPE_KEY) return { finished };
if (finished && finished.scopeKey === CHAT_BASH_SCOPE_KEY) {
return { finished };
}
return {};
}
function ensureActiveJobState() {
if (!activeJob) return null;
if (activeJob.state === "starting") return activeJob;
if (!activeJob) {
return null;
}
if (activeJob.state === "starting") {
return activeJob;
}
const { running, finished } = getScopedSession(activeJob.sessionId);
if (running) return activeJob;
if (running) {
return activeJob;
}
if (finished) {
activeJob = null;
return null;
@@ -120,12 +140,20 @@ function ensureActiveJobState() {
}
function attachActiveWatcher(sessionId: string) {
if (!activeJob || activeJob.state !== "running") return;
if (activeJob.sessionId !== sessionId) return;
if (activeJob.watcherAttached) return;
if (!activeJob || activeJob.state !== "running") {
return;
}
if (activeJob.sessionId !== sessionId) {
return;
}
if (activeJob.watcherAttached) {
return;
}
const { running } = getScopedSession(sessionId);
const child = running?.child;
if (!child) return;
if (!child) {
return;
}
activeJob.watcherAttached = true;
child.once("close", () => {
if (activeJob?.state === "running" && activeJob.sessionId === sessionId) {
@@ -317,7 +345,9 @@ export async function handleBashChatCommand(params: {
}
const commandText = request.command.trim();
if (!commandText) return buildUsageReply();
if (!commandText) {
return buildUsageReply();
}
activeJob = {
state: "starting",

View File

@@ -25,7 +25,9 @@ export function createBlockReplyCoalescer(params: {
let idleTimer: NodeJS.Timeout | undefined;
const clearIdleTimer = () => {
if (!idleTimer) return;
if (!idleTimer) {
return;
}
clearTimeout(idleTimer);
idleTimer = undefined;
};
@@ -37,7 +39,9 @@ export function createBlockReplyCoalescer(params: {
};
const scheduleIdleFlush = () => {
if (idleMs <= 0) return;
if (idleMs <= 0) {
return;
}
clearIdleTimer();
idleTimer = setTimeout(() => {
void flush({ force: false });
@@ -50,7 +54,9 @@ export function createBlockReplyCoalescer(params: {
resetBuffer();
return;
}
if (!bufferText) return;
if (!bufferText) {
return;
}
if (!options?.force && bufferText.length < minChars) {
scheduleIdleFlush();
return;
@@ -65,7 +71,9 @@ export function createBlockReplyCoalescer(params: {
};
const enqueue = (payload: ReplyPayload) => {
if (shouldAbort()) return;
if (shouldAbort()) {
return;
}
const hasMedia = Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0;
const text = payload.text ?? "";
const hasText = text.trim().length > 0;
@@ -74,7 +82,9 @@ export function createBlockReplyCoalescer(params: {
void onFlush(payload);
return;
}
if (!hasText) return;
if (!hasText) {
return;
}
if (
bufferText &&

View File

@@ -53,7 +53,9 @@ const withTimeout = async <T>(
timeoutMs: number,
timeoutError: Error,
): Promise<T> => {
if (!timeoutMs || timeoutMs <= 0) return promise;
if (!timeoutMs || timeoutMs <= 0) {
return promise;
}
let timer: NodeJS.Timeout | undefined;
const timeoutPromise = new Promise<never>((_, reject) => {
timer = setTimeout(() => reject(timeoutError), timeoutMs);
@@ -61,7 +63,9 @@ const withTimeout = async <T>(
try {
return await Promise.race([promise, timeoutPromise]);
} finally {
if (timer) clearTimeout(timer);
if (timer) {
clearTimeout(timer);
}
}
};
@@ -87,20 +91,28 @@ export function createBlockReplyPipeline(params: {
let didLogTimeout = false;
const sendPayload = (payload: ReplyPayload, skipSeen?: boolean) => {
if (aborted) return;
if (aborted) {
return;
}
const payloadKey = createBlockReplyPayloadKey(payload);
if (!skipSeen) {
if (seenKeys.has(payloadKey)) return;
if (seenKeys.has(payloadKey)) {
return;
}
seenKeys.add(payloadKey);
}
if (sentKeys.has(payloadKey) || pendingKeys.has(payloadKey)) return;
if (sentKeys.has(payloadKey) || pendingKeys.has(payloadKey)) {
return;
}
pendingKeys.add(payloadKey);
const timeoutError = new Error(`block reply delivery timed out after ${timeoutMs}ms`);
const abortController = new AbortController();
sendChain = sendChain
.then(async () => {
if (aborted) return false;
if (aborted) {
return false;
}
await withTimeout(
onBlockReply(payload, {
abortSignal: abortController.signal,
@@ -112,7 +124,9 @@ export function createBlockReplyPipeline(params: {
return true;
})
.then((didSend) => {
if (!didSend) return;
if (!didSend) {
return;
}
sentKeys.add(payloadKey);
didStream = true;
})
@@ -148,7 +162,9 @@ export function createBlockReplyPipeline(params: {
const bufferPayload = (payload: ReplyPayload) => {
buffer?.onEnqueue?.(payload);
if (!buffer?.shouldBuffer(payload)) return false;
if (!buffer?.shouldBuffer(payload)) {
return false;
}
const payloadKey = createBlockReplyPayloadKey(payload);
if (
seenKeys.has(payloadKey) ||
@@ -165,7 +181,9 @@ export function createBlockReplyPipeline(params: {
};
const flushBuffered = () => {
if (!bufferedPayloads.length) return;
if (!bufferedPayloads.length) {
return;
}
for (const payload of bufferedPayloads) {
const finalPayload = buffer?.finalize?.(payload) ?? payload;
sendPayload(finalPayload, true);
@@ -175,8 +193,12 @@ export function createBlockReplyPipeline(params: {
};
const enqueue = (payload: ReplyPayload) => {
if (aborted) return;
if (bufferPayload(payload)) return;
if (aborted) {
return;
}
if (bufferPayload(payload)) {
return;
}
const hasMedia = Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0;
if (hasMedia) {
void coalescer?.flush({ force: true });

View File

@@ -16,7 +16,9 @@ const getBlockChunkProviders = () =>
new Set<TextChunkProvider>([...listDeliverableMessageChannels(), INTERNAL_MESSAGE_CHANNEL]);
function normalizeChunkProvider(provider?: string): TextChunkProvider | undefined {
if (!provider) return undefined;
if (!provider) {
return undefined;
}
const cleaned = provider.trim().toLowerCase();
return getBlockChunkProviders().has(cleaned as TextChunkProvider)
? (cleaned as TextChunkProvider)
@@ -34,9 +36,13 @@ function resolveProviderBlockStreamingCoalesce(params: {
accountId?: string | null;
}): BlockStreamingCoalesceConfig | undefined {
const { cfg, providerKey, accountId } = params;
if (!cfg || !providerKey) return undefined;
if (!cfg || !providerKey) {
return undefined;
}
const providerCfg = (cfg as Record<string, unknown>)[providerKey];
if (!providerCfg || typeof providerCfg !== "object") return undefined;
if (!providerCfg || typeof providerCfg !== "object") {
return undefined;
}
const normalizedAccountId = normalizeAccountId(accountId);
const typed = providerCfg as ProviderBlockStreamingConfig;
const accountCfg = typed.accounts?.[normalizedAccountId];

View File

@@ -26,7 +26,9 @@ export async function applySessionHints(params: {
const sessionKey = params.sessionKey;
await updateSessionStore(params.storePath, (store) => {
const entry = store[sessionKey] ?? params.sessionEntry;
if (!entry) return;
if (!entry) {
return;
}
store[sessionKey] = {
...entry,
abortedLastRun: false,

View File

@@ -54,9 +54,13 @@ const SCOPES = new Set<AllowlistScope>(["dm", "group", "all"]);
function parseAllowlistCommand(raw: string): AllowlistCommand | null {
const trimmed = raw.trim();
if (!trimmed.toLowerCase().startsWith("/allowlist")) return null;
if (!trimmed.toLowerCase().startsWith("/allowlist")) {
return null;
}
const rest = trimmed.slice("/allowlist".length).trim();
if (!rest) return { action: "list", scope: "dm" };
if (!rest) {
return { action: "list", scope: "dm" };
}
const tokens = rest.split(/\s+/);
let action: AllowlistAction = "list";
@@ -107,11 +111,15 @@ function parseAllowlistCommand(raw: string): AllowlistCommand | null {
const key = kv[0]?.trim().toLowerCase();
const value = kv[1]?.trim();
if (key === "channel") {
if (value) channel = value;
if (value) {
channel = value;
}
continue;
}
if (key === "account") {
if (value) account = value;
if (value) {
account = value;
}
continue;
}
if (key === "scope" && value && SCOPES.has(value.toLowerCase() as AllowlistScope)) {
@@ -151,7 +159,9 @@ function normalizeAllowFrom(params: {
}
function formatEntryList(entries: string[], resolved?: Map<string, string>): string {
if (entries.length === 0) return "(none)";
if (entries.length === 0) {
return "(none)";
}
return entries
.map((entry) => {
const name = resolved?.get(entry);
@@ -185,7 +195,9 @@ function resolveAccountTarget(
function getNestedValue(root: Record<string, unknown>, path: string[]): unknown {
let current: unknown = root;
for (const key of path) {
if (!current || typeof current !== "object") return undefined;
if (!current || typeof current !== "object") {
return undefined;
}
current = (current as Record<string, unknown>)[key];
}
return current;
@@ -207,7 +219,9 @@ function ensureNestedObject(
}
function setNestedValue(root: Record<string, unknown>, path: string[], value: unknown) {
if (path.length === 0) return;
if (path.length === 0) {
return;
}
if (path.length === 1) {
root[path[0]] = value;
return;
@@ -217,13 +231,17 @@ function setNestedValue(root: Record<string, unknown>, path: string[], value: un
}
function deleteNestedValue(root: Record<string, unknown>, path: string[]) {
if (path.length === 0) return;
if (path.length === 0) {
return;
}
if (path.length === 1) {
delete root[path[0]];
return;
}
const parent = getNestedValue(root, path.slice(0, -1));
if (!parent || typeof parent !== "object") return;
if (!parent || typeof parent !== "object") {
return;
}
delete (parent as Record<string, unknown>)[path[path.length - 1]];
}
@@ -231,9 +249,13 @@ function resolveChannelAllowFromPaths(
channelId: ChannelId,
scope: AllowlistScope,
): string[] | null {
if (scope === "all") return null;
if (scope === "all") {
return null;
}
if (scope === "dm") {
if (channelId === "slack" || channelId === "discord") return ["dm", "allowFrom"];
if (channelId === "slack" || channelId === "discord") {
return ["dm", "allowFrom"];
}
if (
channelId === "telegram" ||
channelId === "whatsapp" ||
@@ -265,11 +287,15 @@ async function resolveSlackNames(params: {
}) {
const account = resolveSlackAccount({ cfg: params.cfg, accountId: params.accountId });
const token = account.config.userToken?.trim() || account.botToken?.trim();
if (!token) return new Map<string, string>();
if (!token) {
return new Map<string, string>();
}
const resolved = await resolveSlackUserAllowlist({ token, entries: params.entries });
const map = new Map<string, string>();
for (const entry of resolved) {
if (entry.resolved && entry.name) map.set(entry.input, entry.name);
if (entry.resolved && entry.name) {
map.set(entry.input, entry.name);
}
}
return map;
}
@@ -281,19 +307,27 @@ async function resolveDiscordNames(params: {
}) {
const account = resolveDiscordAccount({ cfg: params.cfg, accountId: params.accountId });
const token = account.token?.trim();
if (!token) return new Map<string, string>();
if (!token) {
return new Map<string, string>();
}
const resolved = await resolveDiscordUserAllowlist({ token, entries: params.entries });
const map = new Map<string, string>();
for (const entry of resolved) {
if (entry.resolved && entry.name) map.set(entry.input, entry.name);
if (entry.resolved && entry.name) {
map.set(entry.input, entry.name);
}
}
return map;
}
export const handleAllowlistCommand: CommandHandler = async (params, allowTextCommands) => {
if (!allowTextCommands) return null;
if (!allowTextCommands) {
return null;
}
const parsed = parseAllowlistCommand(params.command.commandBodyNormalized);
if (!parsed) return null;
if (!parsed) {
return null;
}
if (parsed.action === "error") {
return { shouldContinue: false, reply: { text: `⚠️ ${parsed.message}` } };
}
@@ -444,8 +478,12 @@ export const handleAllowlistCommand: CommandHandler = async (params, allowTextCo
const lines: string[] = ["🧾 Allowlist"];
lines.push(`Channel: ${channelId}${accountId ? ` (account ${accountId})` : ""}`);
if (dmPolicy) lines.push(`DM policy: ${dmPolicy}`);
if (groupPolicy) lines.push(`Group policy: ${groupPolicy}`);
if (dmPolicy) {
lines.push(`DM policy: ${dmPolicy}`);
}
if (groupPolicy) {
lines.push(`Group policy: ${groupPolicy}`);
}
const showDm = scope === "dm" || scope === "all";
const showGroup = scope === "group" || scope === "all";

View File

@@ -24,7 +24,9 @@ type ParsedApproveCommand =
function parseApproveCommand(raw: string): ParsedApproveCommand | null {
const trimmed = raw.trim();
if (!trimmed.toLowerCase().startsWith(COMMAND)) return null;
if (!trimmed.toLowerCase().startsWith(COMMAND)) {
return null;
}
const rest = trimmed.slice(COMMAND.length).trim();
if (!rest) {
return { ok: false, error: "Usage: /approve <id> allow-once|allow-always|deny" };
@@ -61,10 +63,14 @@ function buildResolvedByLabel(params: Parameters<CommandHandler>[0]): string {
}
export const handleApproveCommand: CommandHandler = async (params, allowTextCommands) => {
if (!allowTextCommands) return null;
if (!allowTextCommands) {
return null;
}
const normalized = params.command.commandBodyNormalized;
const parsed = parseApproveCommand(normalized);
if (!parsed) return null;
if (!parsed) {
return null;
}
if (!params.command.isAuthorizedSender) {
logVerbose(
`Ignoring /approve from unauthorized sender: ${params.command.senderId || "<unknown>"}`,

View File

@@ -3,7 +3,9 @@ import { handleBashChatCommand } from "./bash-command.js";
import type { CommandHandler } from "./commands-types.js";
export const handleBashCommand: CommandHandler = async (params, allowTextCommands) => {
if (!allowTextCommands) return null;
if (!allowTextCommands) {
return null;
}
const { command } = params;
const bashSlashRequested =
command.commandBodyNormalized === "/bash" || command.commandBodyNormalized.startsWith("/bash ");

View File

@@ -25,12 +25,18 @@ function extractCompactInstructions(params: {
? stripMentions(raw, params.ctx, params.cfg, params.agentId)
: raw;
const trimmed = stripped.trim();
if (!trimmed) return undefined;
if (!trimmed) {
return undefined;
}
const lowered = trimmed.toLowerCase();
const prefix = lowered.startsWith("/compact") ? "/compact" : null;
if (!prefix) return undefined;
if (!prefix) {
return undefined;
}
let rest = trimmed.slice(prefix.length).trimStart();
if (rest.startsWith(":")) rest = rest.slice(1).trimStart();
if (rest.startsWith(":")) {
rest = rest.slice(1).trimStart();
}
return rest.length ? rest : undefined;
}
@@ -38,7 +44,9 @@ export const handleCompactCommand: CommandHandler = async (params) => {
const compactRequested =
params.command.commandBodyNormalized === "/compact" ||
params.command.commandBodyNormalized.startsWith("/compact ");
if (!compactRequested) return null;
if (!compactRequested) {
return null;
}
if (!params.command.isAuthorizedSender) {
logVerbose(
`Ignoring /compact from unauthorized sender: ${params.command.senderId || "<unknown>"}`,

View File

@@ -23,9 +23,13 @@ import { parseConfigCommand } from "./config-commands.js";
import { parseDebugCommand } from "./debug-commands.js";
export const handleConfigCommand: CommandHandler = async (params, allowTextCommands) => {
if (!allowTextCommands) return null;
if (!allowTextCommands) {
return null;
}
const configCommand = parseConfigCommand(params.command.commandBodyNormalized);
if (!configCommand) return null;
if (!configCommand) {
return null;
}
if (!params.command.isAuthorizedSender) {
logVerbose(
`Ignoring /config from unauthorized sender: ${params.command.senderId || "<unknown>"}`,
@@ -173,9 +177,13 @@ export const handleConfigCommand: CommandHandler = async (params, allowTextComma
};
export const handleDebugCommand: CommandHandler = async (params, allowTextCommands) => {
if (!allowTextCommands) return null;
if (!allowTextCommands) {
return null;
}
const debugCommand = parseDebugCommand(params.command.commandBodyNormalized);
if (!debugCommand) return null;
if (!debugCommand) {
return null;
}
if (!params.command.isAuthorizedSender) {
logVerbose(
`Ignoring /debug from unauthorized sender: ${params.command.senderId || "<unknown>"}`,

View File

@@ -29,8 +29,12 @@ function formatCharsAndTokens(chars: number): string {
}
function parseContextArgs(commandBodyNormalized: string): string {
if (commandBodyNormalized === "/context") return "";
if (commandBodyNormalized.startsWith("/context ")) return commandBodyNormalized.slice(8).trim();
if (commandBodyNormalized === "/context") {
return "";
}
if (commandBodyNormalized.startsWith("/context ")) {
return commandBodyNormalized.slice(8).trim();
}
return "";
}
@@ -49,7 +53,9 @@ async function resolveContextReport(
params: HandleCommandsParams,
): Promise<SessionSystemPromptReport> {
const existing = params.sessionEntry?.systemPromptReport;
if (existing && existing.source === "run") return existing;
if (existing && existing.source === "run") {
return existing;
}
const workspaceDir = params.workspaceDir;
const bootstrapMaxChars = resolveBootstrapMaxChars(params.cfg);

View File

@@ -110,7 +110,9 @@ export async function handleCommands(params: HandleCommandsParams): Promise<Comm
for (const handler of HANDLERS) {
const result = await handler(params, allowTextCommands);
if (result) return result;
if (result) {
return result;
}
}
const sendPolicy = resolveSendPolicy({

View File

@@ -10,8 +10,12 @@ import { buildContextReply } from "./commands-context-report.js";
import type { CommandHandler } from "./commands-types.js";
export const handleHelpCommand: CommandHandler = async (params, allowTextCommands) => {
if (!allowTextCommands) return null;
if (params.command.commandBodyNormalized !== "/help") return null;
if (!allowTextCommands) {
return null;
}
if (params.command.commandBodyNormalized !== "/help") {
return null;
}
if (!params.command.isAuthorizedSender) {
logVerbose(
`Ignoring /help from unauthorized sender: ${params.command.senderId || "<unknown>"}`,
@@ -25,8 +29,12 @@ export const handleHelpCommand: CommandHandler = async (params, allowTextCommand
};
export const handleCommandsListCommand: CommandHandler = async (params, allowTextCommands) => {
if (!allowTextCommands) return null;
if (params.command.commandBodyNormalized !== "/commands") return null;
if (!allowTextCommands) {
return null;
}
if (params.command.commandBodyNormalized !== "/commands") {
return null;
}
if (!params.command.isAuthorizedSender) {
logVerbose(
`Ignoring /commands from unauthorized sender: ${params.command.senderId || "<unknown>"}`,
@@ -108,10 +116,14 @@ export function buildCommandsPaginationKeyboard(
}
export const handleStatusCommand: CommandHandler = async (params, allowTextCommands) => {
if (!allowTextCommands) return null;
if (!allowTextCommands) {
return null;
}
const statusRequested =
params.directives.hasStatusDirective || params.command.commandBodyNormalized === "/status";
if (!statusRequested) return null;
if (!statusRequested) {
return null;
}
if (!params.command.isAuthorizedSender) {
logVerbose(
`Ignoring /status from unauthorized sender: ${params.command.senderId || "<unknown>"}`,
@@ -140,9 +152,13 @@ export const handleStatusCommand: CommandHandler = async (params, allowTextComma
};
export const handleContextCommand: CommandHandler = async (params, allowTextCommands) => {
if (!allowTextCommands) return null;
if (!allowTextCommands) {
return null;
}
const normalized = params.command.commandBodyNormalized;
if (normalized !== "/context" && !normalized.startsWith("/context ")) return null;
if (normalized !== "/context" && !normalized.startsWith("/context ")) {
return null;
}
if (!params.command.isAuthorizedSender) {
logVerbose(
`Ignoring /context from unauthorized sender: ${params.command.senderId || "<unknown>"}`,
@@ -153,8 +169,12 @@ export const handleContextCommand: CommandHandler = async (params, allowTextComm
};
export const handleWhoamiCommand: CommandHandler = async (params, allowTextCommands) => {
if (!allowTextCommands) return null;
if (params.command.commandBodyNormalized !== "/whoami") return null;
if (!allowTextCommands) {
return null;
}
if (params.command.commandBodyNormalized !== "/whoami") {
return null;
}
if (!params.command.isAuthorizedSender) {
logVerbose(
`Ignoring /whoami from unauthorized sender: ${params.command.senderId || "<unknown>"}`,
@@ -164,7 +184,9 @@ export const handleWhoamiCommand: CommandHandler = async (params, allowTextComma
const senderId = params.ctx.SenderId ?? "";
const senderUsername = params.ctx.SenderUsername ?? "";
const lines = ["🧭 Identity", `Channel: ${params.command.channel}`];
if (senderId) lines.push(`User id: ${senderId}`);
if (senderId) {
lines.push(`User id: ${senderId}`);
}
if (senderUsername) {
const handle = senderUsername.startsWith("@") ? senderUsername : `@${senderUsername}`;
lines.push(`Username: ${handle}`);

View File

@@ -42,12 +42,16 @@ function parseModelsArgs(raw: string): {
}
if (lower.startsWith("page=")) {
const value = Number.parseInt(lower.slice("page=".length), 10);
if (Number.isFinite(value) && value > 0) page = value;
if (Number.isFinite(value) && value > 0) {
page = value;
}
continue;
}
if (/^[0-9]+$/.test(lower)) {
const value = Number.parseInt(lower, 10);
if (Number.isFinite(value) && value > 0) page = value;
if (Number.isFinite(value) && value > 0) {
page = value;
}
}
}
@@ -57,7 +61,9 @@ function parseModelsArgs(raw: string): {
if (lower.startsWith("limit=") || lower.startsWith("size=")) {
const rawValue = lower.slice(lower.indexOf("=") + 1);
const value = Number.parseInt(rawValue, 10);
if (Number.isFinite(value) && value > 0) pageSize = Math.min(PAGE_SIZE_MAX, value);
if (Number.isFinite(value) && value > 0) {
pageSize = Math.min(PAGE_SIZE_MAX, value);
}
}
}
@@ -74,7 +80,9 @@ export async function resolveModelsCommandReply(params: {
commandBodyNormalized: string;
}): Promise<ReplyPayload | null> {
const body = params.commandBodyNormalized.trim();
if (!body.startsWith("/models")) return null;
if (!body.startsWith("/models")) {
return null;
}
const argText = body.replace(/^\/models\b/i, "").trim();
const { provider, page, pageSize, all } = parseModelsArgs(argText);
@@ -108,13 +116,17 @@ export async function resolveModelsCommandReply(params: {
const addRawModelRef = (raw?: string) => {
const trimmed = raw?.trim();
if (!trimmed) return;
if (!trimmed) {
return;
}
const resolved = resolveModelRefFromString({
raw: trimmed,
defaultProvider: resolvedDefault.provider,
aliasIndex,
});
if (!resolved) return;
if (!resolved) {
return;
}
add(resolved.ref.provider, resolved.ref.model);
};
@@ -232,12 +244,16 @@ export async function resolveModelsCommandReply(params: {
}
export const handleModelsCommand: CommandHandler = async (params, allowTextCommands) => {
if (!allowTextCommands) return null;
if (!allowTextCommands) {
return null;
}
const reply = await resolveModelsCommandReply({
cfg: params.cfg,
commandBodyNormalized: params.command.commandBodyNormalized,
});
if (!reply) return null;
if (!reply) {
return null;
}
return { reply, shouldContinue: false };
};

View File

@@ -19,11 +19,15 @@ export const handlePluginCommand: CommandHandler = async (
): Promise<CommandHandlerResult | null> => {
const { command, cfg } = params;
if (!allowTextCommands) return null;
if (!allowTextCommands) {
return null;
}
// Try to match a plugin command
const match = matchPluginCommand(command.commandBodyNormalized);
if (!match) return null;
if (!match) {
return null;
}
// Execute the plugin command (always returns a result)
const result = await executePluginCommand({

View File

@@ -22,9 +22,13 @@ function resolveSessionEntryForKey(
store: Record<string, SessionEntry> | undefined,
sessionKey: string | undefined,
) {
if (!store || !sessionKey) return {};
if (!store || !sessionKey) {
return {};
}
const direct = store[sessionKey];
if (direct) return { entry: direct, key: sessionKey };
if (direct) {
return { entry: direct, key: sessionKey };
}
return {};
}
@@ -36,7 +40,9 @@ function resolveAbortTarget(params: {
}) {
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 (entry && key) {
return { entry, key, sessionId: entry.sessionId };
}
if (params.sessionEntry && params.sessionKey) {
return {
entry: params.sessionEntry,
@@ -48,9 +54,13 @@ function resolveAbortTarget(params: {
}
export const handleActivationCommand: CommandHandler = async (params, allowTextCommands) => {
if (!allowTextCommands) return null;
if (!allowTextCommands) {
return null;
}
const activationCommand = parseActivationCommand(params.command.commandBodyNormalized);
if (!activationCommand.hasCommand) return null;
if (!activationCommand.hasCommand) {
return null;
}
if (!params.isGroup) {
return {
shouldContinue: false,
@@ -89,9 +99,13 @@ export const handleActivationCommand: CommandHandler = async (params, allowTextC
};
export const handleSendPolicyCommand: CommandHandler = async (params, allowTextCommands) => {
if (!allowTextCommands) return null;
if (!allowTextCommands) {
return null;
}
const sendPolicyCommand = parseSendPolicyCommand(params.command.commandBodyNormalized);
if (!sendPolicyCommand.hasCommand) return null;
if (!sendPolicyCommand.hasCommand) {
return null;
}
if (!params.command.isAuthorizedSender) {
logVerbose(
`Ignoring /send from unauthorized sender: ${params.command.senderId || "<unknown>"}`,
@@ -131,9 +145,13 @@ export const handleSendPolicyCommand: CommandHandler = async (params, allowTextC
};
export const handleUsageCommand: CommandHandler = async (params, allowTextCommands) => {
if (!allowTextCommands) return null;
if (!allowTextCommands) {
return null;
}
const normalized = params.command.commandBodyNormalized;
if (normalized !== "/usage" && !normalized.startsWith("/usage ")) return null;
if (normalized !== "/usage" && !normalized.startsWith("/usage ")) {
return null;
}
if (!params.command.isAuthorizedSender) {
logVerbose(
`Ignoring /usage from unauthorized sender: ${params.command.senderId || "<unknown>"}`,
@@ -195,8 +213,11 @@ export const handleUsageCommand: CommandHandler = async (params, allowTextComman
const next = requested ?? (current === "off" ? "tokens" : current === "tokens" ? "full" : "off");
if (params.sessionEntry && params.sessionStore && params.sessionKey) {
if (next === "off") delete params.sessionEntry.responseUsage;
else params.sessionEntry.responseUsage = next;
if (next === "off") {
delete params.sessionEntry.responseUsage;
} else {
params.sessionEntry.responseUsage = next;
}
params.sessionEntry.updatedAt = Date.now();
params.sessionStore[params.sessionKey] = params.sessionEntry;
if (params.storePath) {
@@ -215,8 +236,12 @@ export const handleUsageCommand: CommandHandler = async (params, allowTextComman
};
export const handleRestartCommand: CommandHandler = async (params, allowTextCommands) => {
if (!allowTextCommands) return null;
if (params.command.commandBodyNormalized !== "/restart") return null;
if (!allowTextCommands) {
return null;
}
if (params.command.commandBodyNormalized !== "/restart") {
return null;
}
if (!params.command.isAuthorizedSender) {
logVerbose(
`Ignoring /restart from unauthorized sender: ${params.command.senderId || "<unknown>"}`,
@@ -260,8 +285,12 @@ export const handleRestartCommand: CommandHandler = async (params, allowTextComm
};
export const handleStopCommand: CommandHandler = async (params, allowTextCommands) => {
if (!allowTextCommands) return null;
if (params.command.commandBodyNormalized !== "/stop") return null;
if (!allowTextCommands) {
return null;
}
if (params.command.commandBodyNormalized !== "/stop") {
return null;
}
if (!params.command.isAuthorizedSender) {
logVerbose(
`Ignoring /stop from unauthorized sender: ${params.command.senderId || "<unknown>"}`,
@@ -319,8 +348,12 @@ export const handleStopCommand: CommandHandler = async (params, allowTextCommand
};
export const handleAbortTrigger: CommandHandler = async (params, allowTextCommands) => {
if (!allowTextCommands) return null;
if (!isAbortTrigger(params.command.rawBodyNormalized)) return null;
if (!allowTextCommands) {
return null;
}
if (!isAbortTrigger(params.command.rawBodyNormalized)) {
return null;
}
const abortTarget = resolveAbortTarget({
ctx: params.ctx,
sessionKey: params.sessionKey,

View File

@@ -34,7 +34,9 @@ import { resolveSubagentLabel } from "./subagents-utils.js";
function formatApiKeySnippet(apiKey: string): string {
const compact = apiKey.replace(/\s+/g, "");
if (!compact) return "unknown";
if (!compact) {
return "unknown";
}
const edge = compact.length >= 12 ? 6 : 4;
const head = compact.slice(0, edge);
const tail = compact.slice(-edge);
@@ -48,7 +50,9 @@ function resolveModelAuthLabel(
agentDir?: string,
): string | undefined {
const resolved = provider?.trim();
if (!resolved) return undefined;
if (!resolved) {
return undefined;
}
const providerKey = normalizeProviderId(resolved);
const store = ensureAuthProfileStore(agentDir, {
@@ -161,7 +165,9 @@ export async function buildStatusReply(params: {
maxWindows: 2,
includeResets: true,
});
if (summaryLine) usageLine = `📊 Usage: ${summaryLine}`;
if (summaryLine) {
usageLine = `📊 Usage: ${summaryLine}`;
}
}
} catch {
usageLine = null;

View File

@@ -36,18 +36,24 @@ const COMMAND = "/subagents";
const ACTIONS = new Set(["list", "stop", "log", "send", "info", "help"]);
function formatTimestamp(valueMs?: number) {
if (!valueMs || !Number.isFinite(valueMs) || valueMs <= 0) return "n/a";
if (!valueMs || !Number.isFinite(valueMs) || valueMs <= 0) {
return "n/a";
}
return new Date(valueMs).toISOString();
}
function formatTimestampWithAge(valueMs?: number) {
if (!valueMs || !Number.isFinite(valueMs) || valueMs <= 0) return "n/a";
if (!valueMs || !Number.isFinite(valueMs) || valueMs <= 0) {
return "n/a";
}
return `${formatTimestamp(valueMs)} (${formatAgeShort(Date.now() - valueMs)})`;
}
function resolveRequesterSessionKey(params: Parameters<CommandHandler>[0]): string | undefined {
const raw = params.sessionKey?.trim() || params.ctx.CommandTargetSessionKey?.trim();
if (!raw) return undefined;
if (!raw) {
return undefined;
}
const { mainKey, alias } = resolveMainSessionAlias(params.cfg);
return resolveInternalSessionKey({ key: raw, alias, mainKey });
}
@@ -57,7 +63,9 @@ function resolveSubagentTarget(
token: string | undefined,
): SubagentTargetResolution {
const trimmed = token?.trim();
if (!trimmed) return { error: "Missing subagent id." };
if (!trimmed) {
return { error: "Missing subagent id." };
}
if (trimmed === "last") {
const sorted = sortSubagentRuns(runs);
return { entry: sorted[0] };
@@ -75,7 +83,9 @@ function resolveSubagentTarget(
return match ? { entry: match } : { error: `Unknown subagent session: ${trimmed}` };
}
const byRunId = runs.filter((entry) => entry.runId.startsWith(trimmed));
if (byRunId.length === 1) return { entry: byRunId[0] };
if (byRunId.length === 1) {
return { entry: byRunId[0] };
}
if (byRunId.length > 1) {
return { error: `Ambiguous run id prefix: ${trimmed}` };
}
@@ -117,11 +127,17 @@ export function extractMessageText(message: ChatMessage): { role: string; text:
);
return normalized ? { role, text: normalized } : null;
}
if (!Array.isArray(content)) return null;
if (!Array.isArray(content)) {
return null;
}
const chunks: string[] = [];
for (const block of content) {
if (!block || typeof block !== "object") continue;
if ((block as { type?: unknown }).type !== "text") continue;
if (!block || typeof block !== "object") {
continue;
}
if ((block as { type?: unknown }).type !== "text") {
continue;
}
const text = (block as { text?: unknown }).text;
if (typeof text === "string") {
const value = shouldSanitize ? sanitizeTextContent(text) : text;
@@ -138,7 +154,9 @@ function formatLogLines(messages: ChatMessage[]) {
const lines: string[] = [];
for (const msg of messages) {
const extracted = extractMessageText(msg);
if (!extracted) continue;
if (!extracted) {
continue;
}
const label = extracted.role === "assistant" ? "Assistant" : "User";
lines.push(`${label}: ${extracted.text}`);
}
@@ -153,9 +171,13 @@ function loadSubagentSessionEntry(params: Parameters<CommandHandler>[0], childKe
}
export const handleSubagentsCommand: CommandHandler = async (params, allowTextCommands) => {
if (!allowTextCommands) return null;
if (!allowTextCommands) {
return null;
}
const normalized = params.command.commandBodyNormalized;
if (!normalized.startsWith(COMMAND)) return null;
if (!normalized.startsWith(COMMAND)) {
return null;
}
if (!params.command.isAuthorizedSender) {
logVerbose(
`Ignoring /subagents from unauthorized sender: ${params.command.senderId || "<unknown>"}`,
@@ -361,7 +383,9 @@ export const handleSubagentsCommand: CommandHandler = async (params, allowTextCo
},
timeoutMs: 10_000,
});
if (response?.runId) runId = response.runId;
if (response?.runId) {
runId = response.runId;
}
} catch (err) {
const messageText =
err instanceof Error ? err.message : typeof err === "string" ? err : "error";

View File

@@ -26,10 +26,16 @@ type ParsedTtsCommand = {
function parseTtsCommand(normalized: string): ParsedTtsCommand | null {
// Accept `/tts` and `/tts <action> [args]` as a single control surface.
if (normalized === "/tts") return { action: "status", args: "" };
if (!normalized.startsWith("/tts ")) return null;
if (normalized === "/tts") {
return { action: "status", args: "" };
}
if (!normalized.startsWith("/tts ")) {
return null;
}
const rest = normalized.slice(5).trim();
if (!rest) return { action: "status", args: "" };
if (!rest) {
return { action: "status", args: "" };
}
const [action, ...tail] = rest.split(/\s+/);
return { action: action.toLowerCase(), args: tail.join(" ").trim() };
}
@@ -63,9 +69,13 @@ function ttsUsage(): ReplyPayload {
}
export const handleTtsCommands: CommandHandler = async (params, allowTextCommands) => {
if (!allowTextCommands) return null;
if (!allowTextCommands) {
return null;
}
const parsed = parseTtsCommand(params.command.commandBodyNormalized);
if (!parsed) return null;
if (!parsed) {
return null;
}
if (!params.command.isAuthorizedSender) {
logVerbose(

View File

@@ -8,12 +8,18 @@ export type ConfigCommand =
export function parseConfigCommand(raw: string): ConfigCommand | null {
const trimmed = raw.trim();
if (!trimmed.toLowerCase().startsWith("/config")) return null;
if (!trimmed.toLowerCase().startsWith("/config")) {
return null;
}
const rest = trimmed.slice("/config".length).trim();
if (!rest) return { action: "show" };
if (!rest) {
return { action: "show" };
}
const match = rest.match(/^(\S+)(?:\s+([\s\S]+))?$/);
if (!match) return { action: "error", message: "Invalid /config syntax." };
if (!match) {
return { action: "error", message: "Invalid /config syntax." };
}
const action = match[1].toLowerCase();
const args = (match[2] ?? "").trim();
@@ -23,7 +29,9 @@ export function parseConfigCommand(raw: string): ConfigCommand | null {
case "get":
return { action: "show", path: args || undefined };
case "unset": {
if (!args) return { action: "error", message: "Usage: /config unset path" };
if (!args) {
return { action: "error", message: "Usage: /config unset path" };
}
return { action: "unset", path: args };
}
case "set": {

View File

@@ -3,7 +3,9 @@ export function parseConfigValue(raw: string): {
error?: string;
} {
const trimmed = raw.trim();
if (!trimmed) return { error: "Missing value." };
if (!trimmed) {
return { error: "Missing value." };
}
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
try {
@@ -13,13 +15,21 @@ export function parseConfigValue(raw: string): {
}
}
if (trimmed === "true") return { value: true };
if (trimmed === "false") return { value: false };
if (trimmed === "null") return { value: null };
if (trimmed === "true") {
return { value: true };
}
if (trimmed === "false") {
return { value: false };
}
if (trimmed === "null") {
return { value: null };
}
if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
const num = Number(trimmed);
if (Number.isFinite(num)) return { value: num };
if (Number.isFinite(num)) {
return { value: num };
}
}
if (

View File

@@ -9,12 +9,18 @@ export type DebugCommand =
export function parseDebugCommand(raw: string): DebugCommand | null {
const trimmed = raw.trim();
if (!trimmed.toLowerCase().startsWith("/debug")) return null;
if (!trimmed.toLowerCase().startsWith("/debug")) {
return null;
}
const rest = trimmed.slice("/debug".length).trim();
if (!rest) return { action: "show" };
if (!rest) {
return { action: "show" };
}
const match = rest.match(/^(\S+)(?:\s+([\s\S]+))?$/);
if (!match) return { action: "error", message: "Invalid /debug syntax." };
if (!match) {
return { action: "error", message: "Invalid /debug syntax." };
}
const action = match[1].toLowerCase();
const args = (match[2] ?? "").trim();
@@ -24,7 +30,9 @@ export function parseDebugCommand(raw: string): DebugCommand | null {
case "reset":
return { action: "reset" };
case "unset": {
if (!args) return { action: "error", message: "Usage: /debug unset path" };
if (!args) {
return { action: "error", message: "Usage: /debug unset path" };
}
return { action: "unset", path: args };
}
case "set": {

View File

@@ -17,8 +17,12 @@ export type ModelAuthDetailMode = "compact" | "verbose";
const maskApiKey = (value: string): string => {
const trimmed = value.trim();
if (!trimmed) return "missing";
if (trimmed.length <= 16) return trimmed;
if (!trimmed) {
return "missing";
}
if (trimmed.length <= 16) {
return trimmed;
}
return `${trimmed.slice(0, 8)}...${trimmed.slice(-8)}`;
};
@@ -37,9 +41,13 @@ export const resolveAuthLabel = async (
const providerKey = normalizeProviderId(provider);
const lastGood = (() => {
const map = store.lastGood;
if (!map) return undefined;
if (!map) {
return undefined;
}
for (const [key, value] of Object.entries(map)) {
if (normalizeProviderId(key) === providerKey) return value;
if (normalizeProviderId(key) === providerKey) {
return value;
}
}
return undefined;
})();
@@ -49,10 +57,16 @@ export const resolveAuthLabel = async (
const formatUntil = (timestampMs: number) => {
const remainingMs = Math.max(0, timestampMs - now);
const minutes = Math.round(remainingMs / 60_000);
if (minutes < 1) return "soon";
if (minutes < 60) return `${minutes}m`;
if (minutes < 1) {
return "soon";
}
if (minutes < 60) {
return `${minutes}m`;
}
const hours = Math.round(minutes / 60);
if (hours < 48) return `${hours}h`;
if (hours < 48) {
return `${hours}h`;
}
const days = Math.round(hours / 24);
return `${days}d`;
};
@@ -60,7 +74,9 @@ export const resolveAuthLabel = async (
if (order.length > 0) {
if (mode === "compact") {
const profileId = nextProfileId;
if (!profileId) return { label: "missing", source: "missing" };
if (!profileId) {
return { label: "missing", source: "missing" };
}
const profile = store.profiles[profileId];
const configProfile = cfg.auth?.profiles?.[profileId];
const missing =
@@ -71,7 +87,9 @@ export const resolveAuthLabel = async (
!(configProfile.mode === "oauth" && profile.type === "token"));
const more = order.length > 1 ? ` (+${order.length - 1})` : "";
if (missing) return { label: `${profileId} missing${more}`, source: "" };
if (missing) {
return { label: `${profileId} missing${more}`, source: "" };
}
if (profile.type === "api_key") {
return {
@@ -110,8 +128,12 @@ export const resolveAuthLabel = async (
const profile = store.profiles[profileId];
const configProfile = cfg.auth?.profiles?.[profileId];
const flags: string[] = [];
if (profileId === nextProfileId) flags.push("next");
if (lastGood && profileId === lastGood) flags.push("lastGood");
if (profileId === nextProfileId) {
flags.push("next");
}
if (lastGood && profileId === lastGood) {
flags.push("lastGood");
}
if (isProfileInCooldown(store, profileId)) {
const until = store.usageStats?.[profileId]?.cooldownUntil;
if (typeof until === "number" && Number.isFinite(until) && until > now) {
@@ -205,7 +227,9 @@ export const resolveProfileOverride = (params: {
agentDir?: string;
}): { profileId?: string; error?: string } => {
const raw = params.rawProfile?.trim();
if (!raw) return {};
if (!raw) {
return {};
}
const store = ensureAuthProfileStore(params.agentDir, {
allowKeychainPrompt: false,
});

View File

@@ -133,7 +133,9 @@ export async function handleDirectiveOnly(params: {
allowedModelCatalog,
resetModelOverride,
});
if (modelInfo) return modelInfo;
if (modelInfo) {
return modelInfo;
}
const modelResolution = resolveModelSelectionFromDirective({
directives,
@@ -146,7 +148,9 @@ export async function handleDirectiveOnly(params: {
allowedModelCatalog,
provider,
});
if (modelResolution.errorText) return { text: modelResolution.errorText };
if (modelResolution.errorText) {
return { text: modelResolution.errorText };
}
const modelSelection = modelResolution.modelSelection;
const profileOverride = modelResolution.profileOverride;
@@ -267,7 +271,9 @@ export async function handleDirectiveOnly(params: {
channel: provider,
sessionEntry,
});
if (queueAck) return queueAck;
if (queueAck) {
return queueAck;
}
if (
directives.hasThinkDirective &&
@@ -301,8 +307,11 @@ export async function handleDirectiveOnly(params: {
let reasoningChanged =
directives.hasReasoningDirective && directives.reasoningLevel !== undefined;
if (directives.hasThinkDirective && directives.thinkLevel) {
if (directives.thinkLevel === "off") delete sessionEntry.thinkingLevel;
else sessionEntry.thinkingLevel = directives.thinkLevel;
if (directives.thinkLevel === "off") {
delete sessionEntry.thinkingLevel;
} else {
sessionEntry.thinkingLevel = directives.thinkLevel;
}
}
if (shouldDowngradeXHigh) {
sessionEntry.thinkingLevel = "high";
@@ -311,8 +320,11 @@ export async function handleDirectiveOnly(params: {
applyVerboseOverride(sessionEntry, directives.verboseLevel);
}
if (directives.hasReasoningDirective && directives.reasoningLevel) {
if (directives.reasoningLevel === "off") delete sessionEntry.reasoningLevel;
else sessionEntry.reasoningLevel = directives.reasoningLevel;
if (directives.reasoningLevel === "off") {
delete sessionEntry.reasoningLevel;
} else {
sessionEntry.reasoningLevel = directives.reasoningLevel;
}
reasoningChanged =
directives.reasoningLevel !== prevReasoningLevel && directives.reasoningLevel !== undefined;
}
@@ -351,7 +363,9 @@ export async function handleDirectiveOnly(params: {
delete sessionEntry.queueCap;
delete sessionEntry.queueDrop;
} else if (directives.hasQueueDirective) {
if (directives.queueMode) sessionEntry.queueMode = directives.queueMode;
if (directives.queueMode) {
sessionEntry.queueMode = directives.queueMode;
}
if (typeof directives.debounceMs === "number") {
sessionEntry.queueDebounceMs = directives.debounceMs;
}
@@ -427,14 +441,24 @@ export async function handleDirectiveOnly(params: {
? formatDirectiveAck("Elevated mode set to full (auto-approve).")
: formatDirectiveAck("Elevated mode set to ask (approvals may still apply)."),
);
if (shouldHintDirectRuntime) parts.push(formatElevatedRuntimeHint());
if (shouldHintDirectRuntime) {
parts.push(formatElevatedRuntimeHint());
}
}
if (directives.hasExecDirective && directives.hasExecOptions) {
const execParts: string[] = [];
if (directives.execHost) execParts.push(`host=${directives.execHost}`);
if (directives.execSecurity) execParts.push(`security=${directives.execSecurity}`);
if (directives.execAsk) execParts.push(`ask=${directives.execAsk}`);
if (directives.execNode) execParts.push(`node=${directives.execNode}`);
if (directives.execHost) {
execParts.push(`host=${directives.execHost}`);
}
if (directives.execSecurity) {
execParts.push(`security=${directives.execSecurity}`);
}
if (directives.execAsk) {
execParts.push(`ask=${directives.execAsk}`);
}
if (directives.execNode) {
execParts.push(`node=${directives.execNode}`);
}
if (execParts.length > 0) {
parts.push(formatDirectiveAck(`Exec defaults set (${execParts.join(", ")}).`));
}
@@ -471,6 +495,8 @@ export async function handleDirectiveOnly(params: {
parts.push(formatDirectiveAck(`Queue drop set to ${directives.dropPolicy}.`));
}
const ack = parts.join(" ").trim();
if (!ack && directives.hasStatusDirective) return undefined;
if (!ack && directives.hasStatusDirective) {
return undefined;
}
return { text: ack || "OK." };
}

View File

@@ -34,9 +34,15 @@ const PROVIDER_RANK = new Map<string, number>(
function compareProvidersForPicker(a: string, b: string): number {
const pa = PROVIDER_RANK.get(a);
const pb = PROVIDER_RANK.get(b);
if (pa !== undefined && pb !== undefined) return pa - pb;
if (pa !== undefined) return -1;
if (pb !== undefined) return 1;
if (pa !== undefined && pb !== undefined) {
return pa - pb;
}
if (pa !== undefined) {
return -1;
}
if (pb !== undefined) {
return 1;
}
return a.localeCompare(b);
}
@@ -47,10 +53,14 @@ export function buildModelPickerItems(catalog: ModelPickerCatalogEntry[]): Model
for (const entry of catalog) {
const provider = normalizeProviderId(entry.provider);
const model = entry.id?.trim();
if (!provider || !model) continue;
if (!provider || !model) {
continue;
}
const key = `${provider}/${model}`;
if (seen.has(key)) continue;
if (seen.has(key)) {
continue;
}
seen.add(key);
out.push({ model, provider });
@@ -59,7 +69,9 @@ export function buildModelPickerItems(catalog: ModelPickerCatalogEntry[]): Model
// Sort by provider preference first, then by model name
out.sort((a, b) => {
const providerOrder = compareProvidersForPicker(a.provider, b.provider);
if (providerOrder !== 0) return providerOrder;
if (providerOrder !== 0) {
return providerOrder;
}
return a.model.toLowerCase().localeCompare(b.model.toLowerCase());
});

View File

@@ -43,22 +43,30 @@ function buildModelPickerCatalog(params: {
const pushRef = (ref: { provider: string; model: string }, name?: string) => {
const provider = normalizeProviderId(ref.provider);
const id = String(ref.model ?? "").trim();
if (!provider || !id) return;
if (!provider || !id) {
return;
}
const key = modelKey(provider, id);
if (keys.has(key)) return;
if (keys.has(key)) {
return;
}
keys.add(key);
out.push({ provider, id, name: name ?? id });
};
const pushRaw = (raw?: string) => {
const value = String(raw ?? "").trim();
if (!value) return;
if (!value) {
return;
}
const resolved = resolveModelRefFromString({
raw: value,
defaultProvider: params.defaultProvider,
aliasIndex: params.aliasIndex,
});
if (!resolved) return;
if (!resolved) {
return;
}
pushRef(resolved.ref);
};
@@ -92,9 +100,13 @@ function buildModelPickerCatalog(params: {
const push = (entry: ModelPickerCatalogEntry) => {
const provider = normalizeProviderId(entry.provider);
const id = String(entry.id ?? "").trim();
if (!provider || !id) return;
if (!provider || !id) {
return;
}
const key = modelKey(provider, id);
if (keys.has(key)) return;
if (keys.has(key)) {
return;
}
keys.add(key);
out.push({ provider, id, name: entry.name });
};
@@ -131,7 +143,9 @@ function buildModelPickerCatalog(params: {
defaultProvider: params.defaultProvider,
aliasIndex: params.aliasIndex,
});
if (!resolved) continue;
if (!resolved) {
continue;
}
push({
provider: resolved.ref.provider,
id: resolved.ref.model,
@@ -164,14 +178,18 @@ export async function maybeHandleModelDirectiveInfo(params: {
allowedModelCatalog: Array<{ provider: string; id?: string; name?: string }>;
resetModelOverride: boolean;
}): Promise<ReplyPayload | undefined> {
if (!params.directives.hasModelDirective) return undefined;
if (!params.directives.hasModelDirective) {
return undefined;
}
const rawDirective = params.directives.rawModelDirective?.trim();
const directive = rawDirective?.toLowerCase();
const wantsStatus = directive === "status";
const wantsSummary = !rawDirective;
const wantsLegacyList = directive === "list";
if (!wantsSummary && !wantsStatus && !wantsLegacyList) return undefined;
if (!wantsSummary && !wantsStatus && !wantsLegacyList) {
return undefined;
}
if (params.directives.rawModelProfile) {
return { text: "Auth profile override requires a model selection." };
@@ -209,12 +227,16 @@ export async function maybeHandleModelDirectiveInfo(params: {
const modelsPath = `${params.agentDir}/models.json`;
const formatPath = (value: string) => shortenHomePath(value);
const authMode: ModelAuthDetailMode = "verbose";
if (pickerCatalog.length === 0) return { text: "No models available." };
if (pickerCatalog.length === 0) {
return { text: "No models available." };
}
const authByProvider = new Map<string, string>();
for (const entry of pickerCatalog) {
const provider = normalizeProviderId(entry.provider);
if (authByProvider.has(provider)) continue;
if (authByProvider.has(provider)) {
continue;
}
const auth = await resolveAuthLabel(
provider,
params.cfg,
@@ -250,7 +272,9 @@ export async function maybeHandleModelDirectiveInfo(params: {
for (const provider of byProvider.keys()) {
const models = byProvider.get(provider);
if (!models) continue;
if (!models) {
continue;
}
const authLabel = authByProvider.get(provider) ?? "missing";
const endpoint = resolveProviderEndpointLabel(provider, params.cfg);
const endpointSuffix = endpoint.endpoint

View File

@@ -206,8 +206,9 @@ export function isDirectiveOnly(params: {
!directives.hasExecDirective &&
!directives.hasModelDirective &&
!directives.hasQueueDirective
)
) {
return false;
}
const stripped = stripStructuralPrefixes(cleanedBody ?? "");
const noMentions = isGroup ? stripMentions(stripped, ctx, cfg, agentId) : stripped;
return noMentions.length === 0;

View File

@@ -12,7 +12,9 @@ export function maybeHandleQueueDirective(params: {
sessionEntry?: SessionEntry;
}): ReplyPayload | undefined {
const { directives } = params;
if (!directives.hasQueueDirective) return undefined;
if (!directives.hasQueueDirective) {
return undefined;
}
const wantsStatus =
!directives.queueMode &&

View File

@@ -4,8 +4,12 @@ import type { ElevatedLevel, ReasoningLevel } from "./directives.js";
export const SYSTEM_MARK = "⚙️";
export const formatDirectiveAck = (text: string): string => {
if (!text) return text;
if (text.startsWith(SYSTEM_MARK)) return text;
if (!text) {
return text;
}
if (text.startsWith(SYSTEM_MARK)) {
return text;
}
return `${SYSTEM_MARK} ${text}`;
};
@@ -27,8 +31,12 @@ export const formatElevatedEvent = (level: ElevatedLevel) => {
};
export const formatReasoningEvent = (level: ReasoningLevel) => {
if (level === "stream") return "Reasoning STREAM — emit live <think>.";
if (level === "on") return "Reasoning ON — include <think>.";
if (level === "stream") {
return "Reasoning STREAM — emit live <think>.";
}
if (level === "on") {
return "Reasoning ON — include <think>.";
}
return "Reasoning OFF — hide <think>.";
};

View File

@@ -25,17 +25,25 @@ const matchLevelDirective = (
): { start: number; end: number; rawLevel?: string } | null => {
const namePattern = names.map(escapeRegExp).join("|");
const match = body.match(new RegExp(`(?:^|\\s)\\/(?:${namePattern})(?=$|\\s|:)`, "i"));
if (!match || match.index === undefined) return null;
if (!match || match.index === undefined) {
return null;
}
const start = match.index;
let end = match.index + match[0].length;
let i = end;
while (i < body.length && /\s/.test(body[i])) i += 1;
while (i < body.length && /\s/.test(body[i])) {
i += 1;
}
if (body[i] === ":") {
i += 1;
while (i < body.length && /\s/.test(body[i])) i += 1;
while (i < body.length && /\s/.test(body[i])) {
i += 1;
}
}
const argStart = i;
while (i < body.length && /[A-Za-z-]/.test(body[i])) i += 1;
while (i < body.length && /[A-Za-z-]/.test(body[i])) {
i += 1;
}
const rawLevel = i > argStart ? body.slice(argStart, i) : undefined;
end = i;
return { start, end, rawLevel };
@@ -87,7 +95,9 @@ export function extractThinkDirective(body?: string): {
rawLevel?: string;
hasDirective: boolean;
} {
if (!body) return { cleaned: "", hasDirective: false };
if (!body) {
return { cleaned: "", hasDirective: false };
}
const extracted = extractLevelDirective(body, ["thinking", "think", "t"], normalizeThinkLevel);
return {
cleaned: extracted.cleaned,
@@ -103,7 +113,9 @@ export function extractVerboseDirective(body?: string): {
rawLevel?: string;
hasDirective: boolean;
} {
if (!body) return { cleaned: "", hasDirective: false };
if (!body) {
return { cleaned: "", hasDirective: false };
}
const extracted = extractLevelDirective(body, ["verbose", "v"], normalizeVerboseLevel);
return {
cleaned: extracted.cleaned,
@@ -119,7 +131,9 @@ export function extractNoticeDirective(body?: string): {
rawLevel?: string;
hasDirective: boolean;
} {
if (!body) return { cleaned: "", hasDirective: false };
if (!body) {
return { cleaned: "", hasDirective: false };
}
const extracted = extractLevelDirective(body, ["notice", "notices"], normalizeNoticeLevel);
return {
cleaned: extracted.cleaned,
@@ -135,7 +149,9 @@ export function extractElevatedDirective(body?: string): {
rawLevel?: string;
hasDirective: boolean;
} {
if (!body) return { cleaned: "", hasDirective: false };
if (!body) {
return { cleaned: "", hasDirective: false };
}
const extracted = extractLevelDirective(body, ["elevated", "elev"], normalizeElevatedLevel);
return {
cleaned: extracted.cleaned,
@@ -151,7 +167,9 @@ export function extractReasoningDirective(body?: string): {
rawLevel?: string;
hasDirective: boolean;
} {
if (!body) return { cleaned: "", hasDirective: false };
if (!body) {
return { cleaned: "", hasDirective: false };
}
const extracted = extractLevelDirective(body, ["reasoning", "reason"], normalizeReasoningLevel);
return {
cleaned: extracted.cleaned,
@@ -165,7 +183,9 @@ export function extractStatusDirective(body?: string): {
cleaned: string;
hasDirective: boolean;
} {
if (!body) return { cleaned: "", hasDirective: false };
if (!body) {
return { cleaned: "", hasDirective: false };
}
return extractSimpleDirective(body, ["status"]);
}

View File

@@ -29,7 +29,9 @@ const isInboundAudioContext = (ctx: FinalizedMsgContext): boolean => {
...(Array.isArray(ctx.MediaTypes) ? ctx.MediaTypes : []),
].filter(Boolean) as string[];
const types = rawTypes.map((type) => normalizeMediaType(type));
if (types.some((type) => type === "audio" || type.startsWith("audio/"))) return true;
if (types.some((type) => type === "audio" || type.startsWith("audio/"))) {
return true;
}
const body =
typeof ctx.BodyForCommands === "string"
@@ -42,8 +44,12 @@ const isInboundAudioContext = (ctx: FinalizedMsgContext): boolean => {
? ctx.Body
: "";
const trimmed = body.trim();
if (!trimmed) return false;
if (AUDIO_PLACEHOLDER_RE.test(trimmed)) return true;
if (!trimmed) {
return false;
}
if (AUDIO_PLACEHOLDER_RE.test(trimmed)) {
return true;
}
return AUDIO_HEADER_RE.test(trimmed);
};
@@ -54,7 +60,9 @@ const resolveSessionTtsAuto = (
const targetSessionKey =
ctx.CommandSource === "native" ? ctx.CommandTargetSessionKey?.trim() : undefined;
const sessionKey = (targetSessionKey ?? ctx.SessionKey)?.trim();
if (!sessionKey) return undefined;
if (!sessionKey) {
return undefined;
}
const agentId = resolveSessionAgentId({ sessionKey, config: cfg });
const storePath = resolveStorePath(cfg.session?.store, { agentId });
try {
@@ -94,7 +102,9 @@ export async function dispatchReplyFromConfig(params: {
error?: string;
},
) => {
if (!diagnosticsEnabled) return;
if (!diagnosticsEnabled) {
return;
}
logMessageProcessed({
channel,
chatId,
@@ -108,7 +118,9 @@ export async function dispatchReplyFromConfig(params: {
};
const markProcessing = () => {
if (!canTrackSession || !sessionKey) return;
if (!canTrackSession || !sessionKey) {
return;
}
logMessageQueued({ sessionKey, channel, source: "dispatch" });
logSessionStateChange({
sessionKey,
@@ -118,7 +130,9 @@ export async function dispatchReplyFromConfig(params: {
};
const markIdle = (reason: string) => {
if (!canTrackSession || !sessionKey) return;
if (!canTrackSession || !sessionKey) {
return;
}
logSessionStateChange({
sessionKey,
state: "idle",
@@ -210,8 +224,12 @@ export async function dispatchReplyFromConfig(params: {
): Promise<void> => {
// TypeScript doesn't narrow these from the shouldRouteToOriginating check,
// but they're guaranteed non-null when this function is called.
if (!originatingChannel || !originatingTo) return;
if (abortSignal?.aborted) return;
if (!originatingChannel || !originatingTo) {
return;
}
if (abortSignal?.aborted) {
return;
}
const result = await routeReply({
payload,
channel: originatingChannel,
@@ -249,7 +267,9 @@ export async function dispatchReplyFromConfig(params: {
cfg,
});
queuedFinal = result.ok;
if (result.ok) routedFinalCount += 1;
if (result.ok) {
routedFinalCount += 1;
}
if (!result.ok) {
logVerbose(
`dispatch-from-config: route-reply (abort) failed: ${result.error ?? "unknown error"}`,
@@ -357,7 +377,9 @@ export async function dispatchReplyFromConfig(params: {
);
}
queuedFinal = result.ok || queuedFinal;
if (result.ok) routedFinalCount += 1;
if (result.ok) {
routedFinalCount += 1;
}
} else {
queuedFinal = dispatcher.sendFinalReply(ttsReply) || queuedFinal;
}
@@ -400,7 +422,9 @@ export async function dispatchReplyFromConfig(params: {
cfg,
});
queuedFinal = result.ok || queuedFinal;
if (result.ok) routedFinalCount += 1;
if (result.ok) {
routedFinalCount += 1;
}
if (!result.ok) {
logVerbose(
`dispatch-from-config: route-reply (tts-only) failed: ${result.error ?? "unknown error"}`,

View File

@@ -20,15 +20,17 @@ type ExecDirectiveParse = {
function normalizeExecHost(value?: string): ExecHost | undefined {
const normalized = value?.trim().toLowerCase();
if (normalized === "sandbox" || normalized === "gateway" || normalized === "node")
if (normalized === "sandbox" || normalized === "gateway" || normalized === "node") {
return normalized;
}
return undefined;
}
function normalizeExecSecurity(value?: string): ExecSecurity | undefined {
const normalized = value?.trim().toLowerCase();
if (normalized === "deny" || normalized === "allowlist" || normalized === "full")
if (normalized === "deny" || normalized === "allowlist" || normalized === "full") {
return normalized;
}
return undefined;
}
@@ -48,10 +50,14 @@ function parseExecDirectiveArgs(raw: string): Omit<
} {
let i = 0;
const len = raw.length;
while (i < len && /\s/.test(raw[i])) i += 1;
while (i < len && /\s/.test(raw[i])) {
i += 1;
}
if (raw[i] === ":") {
i += 1;
while (i < len && /\s/.test(raw[i])) i += 1;
while (i < len && /\s/.test(raw[i])) {
i += 1;
}
}
let consumed = i;
let execHost: ExecHost | undefined;
@@ -69,12 +75,20 @@ function parseExecDirectiveArgs(raw: string): Omit<
let invalidNode = false;
const takeToken = (): string | null => {
if (i >= len) return null;
if (i >= len) {
return null;
}
const start = i;
while (i < len && !/\s/.test(raw[i])) i += 1;
if (start === i) return null;
while (i < len && !/\s/.test(raw[i])) {
i += 1;
}
if (start === i) {
return null;
}
const token = raw.slice(start, i);
while (i < len && /\s/.test(raw[i])) i += 1;
while (i < len && /\s/.test(raw[i])) {
i += 1;
}
return token;
};
@@ -82,23 +96,33 @@ function parseExecDirectiveArgs(raw: string): Omit<
const eq = token.indexOf("=");
const colon = token.indexOf(":");
const idx = eq === -1 ? colon : colon === -1 ? eq : Math.min(eq, colon);
if (idx === -1) return null;
if (idx === -1) {
return null;
}
const key = token.slice(0, idx).trim().toLowerCase();
const value = token.slice(idx + 1).trim();
if (!key) return null;
if (!key) {
return null;
}
return { key, value };
};
while (i < len) {
const token = takeToken();
if (!token) break;
if (!token) {
break;
}
const parsed = splitToken(token);
if (!parsed) break;
if (!parsed) {
break;
}
const { key, value } = parsed;
if (key === "host") {
rawExecHost = value;
execHost = normalizeExecHost(value);
if (!execHost) invalidHost = true;
if (!execHost) {
invalidHost = true;
}
hasExecOptions = true;
consumed = i;
continue;
@@ -106,7 +130,9 @@ function parseExecDirectiveArgs(raw: string): Omit<
if (key === "security") {
rawExecSecurity = value;
execSecurity = normalizeExecSecurity(value);
if (!execSecurity) invalidSecurity = true;
if (!execSecurity) {
invalidSecurity = true;
}
hasExecOptions = true;
consumed = i;
continue;
@@ -114,7 +140,9 @@ function parseExecDirectiveArgs(raw: string): Omit<
if (key === "ask") {
rawExecAsk = value;
execAsk = normalizeExecAsk(value);
if (!execAsk) invalidAsk = true;
if (!execAsk) {
invalidAsk = true;
}
hasExecOptions = true;
consumed = i;
continue;

View File

@@ -172,7 +172,9 @@ export function createFollowupRunner(params: {
runId,
blockReplyBreak: queued.run.blockReplyBreak,
onAgentEvent: (evt) => {
if (evt.stream !== "compaction") return;
if (evt.stream !== "compaction") {
return;
}
const phase = typeof evt.data.phase === "string" ? evt.data.phase : "";
const willRetry = Boolean(evt.data.willRetry);
if (phase === "end" && !willRetry) {
@@ -212,13 +214,19 @@ export function createFollowupRunner(params: {
}
const payloadArray = runResult.payloads ?? [];
if (payloadArray.length === 0) return;
if (payloadArray.length === 0) {
return;
}
const sanitizedPayloads = payloadArray.flatMap((payload) => {
const text = payload.text;
if (!text || !text.includes("HEARTBEAT_OK")) return [payload];
if (!text || !text.includes("HEARTBEAT_OK")) {
return [payload];
}
const stripped = stripHeartbeatToken(text, { mode: "message" });
const hasMedia = Boolean(payload.mediaUrl) || (payload.mediaUrls?.length ?? 0) > 0;
if (stripped.shouldSkip && !hasMedia) return [];
if (stripped.shouldSkip && !hasMedia) {
return [];
}
return [{ ...payload, text: stripped.text }];
});
const replyToChannel =
@@ -249,7 +257,9 @@ export function createFollowupRunner(params: {
});
const finalPayloads = suppressMessagingToolReplies ? [] : dedupedPayloads;
if (finalPayloads.length === 0) return;
if (finalPayloads.length === 0) {
return;
}
if (autoCompactionCompleted) {
const count = await incrementCompactionCount({

View File

@@ -73,7 +73,9 @@ function resolveExecOverrides(params: {
(params.sessionEntry?.execSecurity as ExecOverrides["security"]);
const ask = params.directives.execAsk ?? (params.sessionEntry?.execAsk as ExecOverrides["ask"]);
const node = params.directives.execNode ?? params.sessionEntry?.execNode;
if (!host && !security && !ask && !node) return undefined;
if (!host && !security && !ask && !node) {
return undefined;
}
return { host, security, ask, node };
}
@@ -270,7 +272,9 @@ export async function resolveReplyDirectives(params: {
};
const existingBody = sessionCtx.BodyStripped ?? sessionCtx.Body ?? "";
let cleanedBody = (() => {
if (!existingBody) return parsedDirectives.cleaned;
if (!existingBody) {
return parsedDirectives.cleaned;
}
if (!sessionCtx.CommandBody && !sessionCtx.RawBody) {
return parseInlineDirectives(existingBody, {
modelAliases: configuredAliases,

View File

@@ -26,17 +26,23 @@ export type InlineActionResult =
};
function extractTextFromToolResult(result: any): string | null {
if (!result || typeof result !== "object") return null;
if (!result || typeof result !== "object") {
return null;
}
const content = (result as { content?: unknown }).content;
if (typeof content === "string") {
const trimmed = content.trim();
return trimmed ? trimmed : null;
}
if (!Array.isArray(content)) return null;
if (!Array.isArray(content)) {
return null;
}
const parts: string[] = [];
for (const block of content) {
if (!block || typeof block !== "object") continue;
if (!block || typeof block !== "object") {
continue;
}
const rec = block as { type?: unknown; text?: unknown };
if (rec.type === "text" && typeof rec.text === "string") {
parts.push(rec.text);
@@ -212,8 +218,12 @@ export async function handleInlineActions(params: {
}
const sendInlineReply = async (reply?: ReplyPayload) => {
if (!reply) return;
if (!opts?.onBlockReply) return;
if (!reply) {
return;
}
if (!opts?.onBlockReply) {
return;
}
await opts.onBlockReply(reply);
};

View File

@@ -8,7 +8,9 @@ import type { TemplateContext } from "../templating.js";
function extractGroupId(raw: string | undefined | null): string | undefined {
const trimmed = (raw ?? "").trim();
if (!trimmed) return undefined;
if (!trimmed) {
return undefined;
}
const parts = trimmed.split(":").filter(Boolean);
if (parts.length >= 3 && (parts[1] === "group" || parts[1] === "channel")) {
return parts.slice(2).join(":") || undefined;
@@ -34,7 +36,9 @@ export function resolveGroupRequireMention(params: {
const { cfg, ctx, groupResolution } = params;
const rawChannel = groupResolution?.channel ?? ctx.Provider?.trim();
const channel = normalizeChannelId(rawChannel);
if (!channel) return true;
if (!channel) {
return true;
}
const groupId = groupResolution?.id ?? extractGroupId(ctx.From);
const groupChannel = ctx.GroupChannel?.trim() ?? ctx.GroupSubject?.trim();
const groupSpace = ctx.GroupSpace?.trim();
@@ -45,7 +49,9 @@ export function resolveGroupRequireMention(params: {
groupSpace,
accountId: ctx.AccountId,
});
if (typeof requireMention === "boolean") return requireMention;
if (typeof requireMention === "boolean") {
return requireMention;
}
return true;
}
@@ -68,9 +74,15 @@ export function buildGroupIntro(params: {
const providerKey = rawProvider?.toLowerCase() ?? "";
const providerId = normalizeChannelId(rawProvider);
const providerLabel = (() => {
if (!providerKey) return "chat";
if (isInternalMessageChannel(providerKey)) return "WebChat";
if (providerId) return getChannelPlugin(providerId)?.meta.label ?? providerId;
if (!providerKey) {
return "chat";
}
if (isInternalMessageChannel(providerKey)) {
return "WebChat";
}
if (providerId) {
return getChannelPlugin(providerId)?.meta.label ?? providerId;
}
return `${providerKey.at(0)?.toUpperCase() ?? ""}${providerKey.slice(1)}`;
})();
const subjectLine = subject

View File

@@ -14,12 +14,16 @@ export function evictOldHistoryKeys<T>(
historyMap: Map<string, T[]>,
maxKeys: number = MAX_HISTORY_KEYS,
): void {
if (historyMap.size <= maxKeys) return;
if (historyMap.size <= maxKeys) {
return;
}
const keysToDelete = historyMap.size - maxKeys;
const iterator = historyMap.keys();
for (let i = 0; i < keysToDelete; i++) {
const key = iterator.next().value;
if (key !== undefined) historyMap.delete(key);
if (key !== undefined) {
historyMap.delete(key);
}
}
}
@@ -37,7 +41,9 @@ export function buildHistoryContext(params: {
}): string {
const { historyText, currentMessage } = params;
const lineBreak = params.lineBreak ?? "\n";
if (!historyText.trim()) return currentMessage;
if (!historyText.trim()) {
return currentMessage;
}
return [HISTORY_CONTEXT_MARKER, historyText, "", CURRENT_MESSAGE_MARKER, currentMessage].join(
lineBreak,
);
@@ -50,10 +56,14 @@ export function appendHistoryEntry<T extends HistoryEntry>(params: {
limit: number;
}): T[] {
const { historyMap, historyKey, entry } = params;
if (params.limit <= 0) return [];
if (params.limit <= 0) {
return [];
}
const history = historyMap.get(historyKey) ?? [];
history.push(entry);
while (history.length > params.limit) history.shift();
while (history.length > params.limit) {
history.shift();
}
if (historyMap.has(historyKey)) {
// Refresh insertion order so eviction keeps recently used histories.
historyMap.delete(historyKey);
@@ -79,8 +89,12 @@ export function recordPendingHistoryEntryIfEnabled<T extends HistoryEntry>(param
entry?: T | null;
limit: number;
}): T[] {
if (!params.entry) return [];
if (params.limit <= 0) return [];
if (!params.entry) {
return [];
}
if (params.limit <= 0) {
return [];
}
return recordPendingHistoryEntry({
historyMap: params.historyMap,
historyKey: params.historyKey,
@@ -97,7 +111,9 @@ export function buildPendingHistoryContextFromMap(params: {
formatEntry: (entry: HistoryEntry) => string;
lineBreak?: string;
}): string {
if (params.limit <= 0) return params.currentMessage;
if (params.limit <= 0) {
return params.currentMessage;
}
const entries = params.historyMap.get(params.historyKey) ?? [];
return buildHistoryContextFromEntries({
entries,
@@ -118,7 +134,9 @@ export function buildHistoryContextFromMap(params: {
lineBreak?: string;
excludeLast?: boolean;
}): string {
if (params.limit <= 0) return params.currentMessage;
if (params.limit <= 0) {
return params.currentMessage;
}
const entries = params.entry
? appendHistoryEntry({
historyMap: params.historyMap,
@@ -148,7 +166,9 @@ export function clearHistoryEntriesIfEnabled(params: {
historyKey: string;
limit: number;
}): void {
if (params.limit <= 0) return;
if (params.limit <= 0) {
return;
}
clearHistoryEntries({ historyMap: params.historyMap, historyKey: params.historyKey });
}
@@ -161,7 +181,9 @@ export function buildHistoryContextFromEntries(params: {
}): string {
const lineBreak = params.lineBreak ?? "\n";
const entries = params.excludeLast === false ? params.entries : params.entries.slice(0, -1);
if (entries.length === 0) return params.currentMessage;
if (entries.length === 0) {
return params.currentMessage;
}
const historyText = entries.map(params.formatEntry).join(lineBreak);
return buildHistoryContext({
historyText,

View File

@@ -12,7 +12,9 @@ export type FinalizeInboundContextOptions = {
};
function normalizeTextField(value: unknown): string | undefined {
if (typeof value !== "string") return undefined;
if (typeof value !== "string") {
return undefined;
}
return normalizeInboundTextNewlines(value);
}
@@ -51,7 +53,9 @@ export function finalizeInboundContext<T extends Record<string, unknown>>(
const explicitLabel = normalized.ConversationLabel?.trim();
if (opts.forceConversationLabel || !explicitLabel) {
const resolved = resolveConversationLabel(normalized)?.trim();
if (resolved) normalized.ConversationLabel = resolved;
if (resolved) {
normalized.ConversationLabel = resolved;
}
} else {
normalized.ConversationLabel = explicitLabel;
}

View File

@@ -18,9 +18,13 @@ const resolveInboundPeerId = (ctx: MsgContext) =>
export function buildInboundDedupeKey(ctx: MsgContext): string | null {
const provider = normalizeProvider(ctx.OriginatingChannel ?? ctx.Provider ?? ctx.Surface);
const messageId = ctx.MessageSid?.trim();
if (!provider || !messageId) return null;
if (!provider || !messageId) {
return null;
}
const peerId = resolveInboundPeerId(ctx);
if (!peerId) return null;
if (!peerId) {
return null;
}
const sessionKey = ctx.SessionKey?.trim() ?? "";
const accountId = ctx.AccountId?.trim() ?? "";
const threadId =
@@ -35,7 +39,9 @@ export function shouldSkipDuplicateInbound(
opts?: { cache?: DedupeCache; now?: number },
): boolean {
const key = buildInboundDedupeKey(ctx);
if (!key) return false;
if (!key) {
return false;
}
const cache = opts?.cache ?? inboundDedupeCache;
const skipped = cache.check(key, opts?.now);
if (skipped && shouldLogVerbose()) {

View File

@@ -4,10 +4,16 @@ import { listSenderLabelCandidates, resolveSenderLabel } from "../../channels/se
export function formatInboundBodyWithSenderMeta(params: { body: string; ctx: MsgContext }): string {
const body = params.body;
if (!body.trim()) return body;
if (!body.trim()) {
return body;
}
const chatType = normalizeChatType(params.ctx.ChatType);
if (!chatType || chatType === "direct") return body;
if (hasSenderMetaLine(body, params.ctx)) return body;
if (!chatType || chatType === "direct") {
return body;
}
if (hasSenderMetaLine(body, params.ctx)) {
return body;
}
const senderLabel = resolveSenderLabel({
name: params.ctx.SenderName,
@@ -16,13 +22,17 @@ export function formatInboundBodyWithSenderMeta(params: { body: string; ctx: Msg
e164: params.ctx.SenderE164,
id: params.ctx.SenderId,
});
if (!senderLabel) return body;
if (!senderLabel) {
return body;
}
return `${body}\n[from: ${senderLabel}]`;
}
function hasSenderMetaLine(body: string, ctx: MsgContext): boolean {
if (/(^|\n)\[from:/i.test(body)) return true;
if (/(^|\n)\[from:/i.test(body)) {
return true;
}
const candidates = listSenderLabelCandidates({
name: ctx.SenderName,
username: ctx.SenderUsername,
@@ -30,7 +40,9 @@ function hasSenderMetaLine(body: string, ctx: MsgContext): boolean {
e164: ctx.SenderE164,
id: ctx.SenderId,
});
if (candidates.length === 0) return false;
if (candidates.length === 0) {
return false;
}
return candidates.some((candidate) => {
const escaped = escapeRegExp(candidate);
// Envelope bodies look like "[Signal ...] Alice: hi".

View File

@@ -26,7 +26,9 @@ import {
*/
export function parseLineDirectives(payload: ReplyPayload): ReplyPayload {
let text = payload.text;
if (!text) return payload;
if (!text) {
return payload;
}
const result: ReplyPayload = { ...payload };
const lineData: LineChannelData = {
@@ -121,9 +123,13 @@ export function parseLineDirectives(payload: ReplyPayload): ReplyPayload {
// Find first colon delimiter, ignoring URLs without a label.
const colonIndex = (() => {
const index = trimmed.indexOf(":");
if (index === -1) return -1;
if (index === -1) {
return -1;
}
const lower = trimmed.toLowerCase();
if (lower.startsWith("http://") || lower.startsWith("https://")) return -1;
if (lower.startsWith("http://") || lower.startsWith("https://")) {
return -1;
}
return index;
})();

View File

@@ -28,7 +28,9 @@ export type MemoryFlushSettings = {
};
const normalizeNonNegativeInt = (value: unknown): number | null => {
if (typeof value !== "number" || !Number.isFinite(value)) return null;
if (typeof value !== "number" || !Number.isFinite(value)) {
return null;
}
const int = Math.floor(value);
return int >= 0 ? int : null;
};
@@ -36,7 +38,9 @@ const normalizeNonNegativeInt = (value: unknown): number | null => {
export function resolveMemoryFlushSettings(cfg?: OpenClawConfig): MemoryFlushSettings | null {
const defaults = cfg?.agents?.defaults?.compaction?.memoryFlush;
const enabled = defaults?.enabled ?? true;
if (!enabled) return null;
if (!enabled) {
return null;
}
const softThresholdTokens =
normalizeNonNegativeInt(defaults?.softThresholdTokens) ?? DEFAULT_MEMORY_FLUSH_SOFT_TOKENS;
const prompt = defaults?.prompt?.trim() || DEFAULT_MEMORY_FLUSH_PROMPT;
@@ -55,7 +59,9 @@ export function resolveMemoryFlushSettings(cfg?: OpenClawConfig): MemoryFlushSet
}
function ensureNoReplyHint(text: string): string {
if (text.includes(SILENT_REPLY_TOKEN)) return text;
if (text.includes(SILENT_REPLY_TOKEN)) {
return text;
}
return `${text}\n\nIf no user-visible reply is needed, start with ${SILENT_REPLY_TOKEN}.`;
}
@@ -75,13 +81,19 @@ export function shouldRunMemoryFlush(params: {
softThresholdTokens: number;
}): boolean {
const totalTokens = params.entry?.totalTokens;
if (!totalTokens || totalTokens <= 0) return false;
if (!totalTokens || totalTokens <= 0) {
return false;
}
const contextWindow = Math.max(1, Math.floor(params.contextWindowTokens));
const reserveTokens = Math.max(0, Math.floor(params.reserveTokensFloor));
const softThreshold = Math.max(0, Math.floor(params.softThresholdTokens));
const threshold = Math.max(0, contextWindow - reserveTokens - softThreshold);
if (threshold <= 0) return false;
if (totalTokens < threshold) return false;
if (threshold <= 0) {
return false;
}
if (totalTokens < threshold) {
return false;
}
const compactionCount = params.entry?.compactionCount ?? 0;
const lastFlushAt = params.entry?.memoryFlushCompactionCount;

View File

@@ -28,7 +28,9 @@ const BACKSPACE_CHAR = "\u0008";
export const CURRENT_MESSAGE_MARKER = "[Current message - respond to this]";
function normalizeMentionPattern(pattern: string): string {
if (!pattern.includes(BACKSPACE_CHAR)) return pattern;
if (!pattern.includes(BACKSPACE_CHAR)) {
return pattern;
}
return pattern.split(BACKSPACE_CHAR).join("\\b");
}
@@ -37,7 +39,9 @@ function normalizeMentionPatterns(patterns: string[]): string[] {
}
function resolveMentionPatterns(cfg: OpenClawConfig | undefined, agentId?: string): string[] {
if (!cfg) return [];
if (!cfg) {
return [];
}
const agentConfig = agentId ? resolveAgentConfig(cfg, agentId) : undefined;
const agentGroupChat = agentConfig?.groupChat;
if (agentGroupChat && Object.hasOwn(agentGroupChat, "mentionPatterns")) {
@@ -69,9 +73,13 @@ export function normalizeMentionText(text: string): string {
}
export function matchesMentionPatterns(text: string, mentionRegexes: RegExp[]): boolean {
if (mentionRegexes.length === 0) return false;
if (mentionRegexes.length === 0) {
return false;
}
const cleaned = normalizeMentionText(text ?? "");
if (!cleaned) return false;
if (!cleaned) {
return false;
}
return mentionRegexes.some((re) => re.test(cleaned));
}
@@ -93,7 +101,9 @@ export function matchesMentionWithExplicit(params: {
if (hasAnyMention && explicitAvailable) {
return explicit || params.mentionRegexes.some((re) => re.test(cleaned));
}
if (!cleaned) return explicit;
if (!cleaned) {
return explicit;
}
return explicit || params.mentionRegexes.some((re) => re.test(cleaned));
}

View File

@@ -48,11 +48,17 @@ const FUZZY_VARIANT_TOKENS = [
];
function boundedLevenshteinDistance(a: string, b: string, maxDistance: number): number | null {
if (a === b) return 0;
if (!a || !b) return null;
if (a === b) {
return 0;
}
if (!a || !b) {
return null;
}
const aLen = a.length;
const bLen = b.length;
if (Math.abs(aLen - bLen) > maxDistance) return null;
if (Math.abs(aLen - bLen) > maxDistance) {
return null;
}
// Standard DP with early exit. O(maxDistance * minLen) in common cases.
const prev = Array.from({ length: bLen + 1 }, (_, idx) => idx);
@@ -66,16 +72,24 @@ function boundedLevenshteinDistance(a: string, b: string, maxDistance: number):
for (let j = 1; j <= bLen; j++) {
const cost = aChar === b.charCodeAt(j - 1) ? 0 : 1;
curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
if (curr[j] < rowMin) rowMin = curr[j];
if (curr[j] < rowMin) {
rowMin = curr[j];
}
}
if (rowMin > maxDistance) return null;
if (rowMin > maxDistance) {
return null;
}
for (let j = 0; j <= bLen; j++) prev[j] = curr[j] ?? 0;
for (let j = 0; j <= bLen; j++) {
prev[j] = curr[j] ?? 0;
}
}
const dist = prev[bLen] ?? null;
if (dist == null || dist > maxDistance) return null;
if (dist == null || dist > maxDistance) {
return null;
}
return dist;
}
@@ -90,7 +104,9 @@ function resolveModelOverrideFromEntry(entry?: SessionEntry): {
model: string;
} | null {
const model = entry?.modelOverride?.trim();
if (!model) return null;
if (!model) {
return null;
}
const provider = entry?.providerOverride?.trim() || undefined;
return { provider, model };
}
@@ -100,9 +116,13 @@ function resolveParentSessionKeyCandidate(params: {
parentSessionKey?: string;
}): string | null {
const explicit = params.parentSessionKey?.trim();
if (explicit && explicit !== params.sessionKey) return explicit;
if (explicit && explicit !== params.sessionKey) {
return explicit;
}
const derived = resolveThreadParentSessionKey(params.sessionKey);
if (derived && derived !== params.sessionKey) return derived;
if (derived && derived !== params.sessionKey) {
return derived;
}
return null;
}
@@ -113,15 +133,21 @@ function resolveStoredModelOverride(params: {
parentSessionKey?: string;
}): StoredModelOverride | null {
const direct = resolveModelOverrideFromEntry(params.sessionEntry);
if (direct) return { ...direct, source: "session" };
if (direct) {
return { ...direct, source: "session" };
}
const parentKey = resolveParentSessionKeyCandidate({
sessionKey: params.sessionKey,
parentSessionKey: params.parentSessionKey,
});
if (!parentKey || !params.sessionStore) return null;
if (!parentKey || !params.sessionStore) {
return null;
}
const parentEntry = params.sessionStore[parentKey];
const parentOverride = resolveModelOverrideFromEntry(parentEntry);
if (!parentOverride) return null;
if (!parentOverride) {
return null;
}
return { ...parentOverride, source: "parent" };
}
@@ -152,11 +178,19 @@ function scoreFuzzyMatch(params: {
value: string,
weights: { exact: number; starts: number; includes: number },
) => {
if (!fragment) return 0;
if (!fragment) {
return 0;
}
let score = 0;
if (value === fragment) score = Math.max(score, weights.exact);
if (value.startsWith(fragment)) score = Math.max(score, weights.starts);
if (value.includes(fragment)) score = Math.max(score, weights.includes);
if (value === fragment) {
score = Math.max(score, weights.exact);
}
if (value.startsWith(fragment)) {
score = Math.max(score, weights.starts);
}
if (value.includes(fragment)) {
score = Math.max(score, weights.includes);
}
return score;
};
@@ -200,13 +234,19 @@ function scoreFuzzyMatch(params: {
if (fragmentVariants.length === 0 && variantCount > 0) {
score -= variantCount * 30;
} else if (fragmentVariants.length > 0) {
if (variantMatchCount > 0) score += variantMatchCount * 40;
if (variantMatchCount === 0) score -= 20;
if (variantMatchCount > 0) {
score += variantMatchCount * 40;
}
if (variantMatchCount === 0) {
score -= 20;
}
}
const defaultProvider = normalizeProviderId(params.defaultProvider);
const isDefault = provider === defaultProvider && model === params.defaultModel;
if (isDefault) score += 20;
if (isDefault) {
score += 20;
}
return {
score,
@@ -331,7 +371,9 @@ export async function createModelSelectionState(params: {
let defaultThinkingLevel: ThinkLevel | undefined;
const resolveDefaultThinkingLevel = async () => {
if (defaultThinkingLevel) return defaultThinkingLevel;
if (defaultThinkingLevel) {
return defaultThinkingLevel;
}
let catalogForThinking = modelCatalog ?? allowedModelCatalog;
if (!catalogForThinking || catalogForThinking.length === 0) {
modelCatalog = await loadModelCatalog({ config: cfg });
@@ -389,17 +431,23 @@ export function resolveModelDirectiveSelection(params: {
fragment: string;
}): { selection?: ModelDirectiveSelection; error?: string } => {
const fragment = params.fragment.trim().toLowerCase();
if (!fragment) return {};
if (!fragment) {
return {};
}
const providerFilter = params.provider ? normalizeProviderId(params.provider) : undefined;
const candidates: Array<{ provider: string; model: string }> = [];
for (const key of allowedModelKeys) {
const slash = key.indexOf("/");
if (slash <= 0) continue;
if (slash <= 0) {
continue;
}
const provider = normalizeProviderId(key.slice(0, slash));
const model = key.slice(slash + 1);
if (providerFilter && provider !== providerFilter) continue;
if (providerFilter && provider !== providerFilter) {
continue;
}
candidates.push({ provider, model });
}
@@ -407,7 +455,9 @@ export function resolveModelDirectiveSelection(params: {
if (!params.provider) {
const aliasMatches: Array<{ provider: string; model: string }> = [];
for (const [aliasKey, entry] of aliasIndex.byAlias.entries()) {
if (!aliasKey.includes(fragment)) continue;
if (!aliasKey.includes(fragment)) {
continue;
}
aliasMatches.push({
provider: entry.ref.provider,
model: entry.ref.model,
@@ -415,14 +465,18 @@ export function resolveModelDirectiveSelection(params: {
}
for (const match of aliasMatches) {
const key = modelKey(match.provider, match.model);
if (!allowedModelKeys.has(key)) continue;
if (!allowedModelKeys.has(key)) {
continue;
}
if (!candidates.some((c) => c.provider === match.provider && c.model === match.model)) {
candidates.push(match);
}
}
}
if (candidates.length === 0) return {};
if (candidates.length === 0) {
return {};
}
const scored = candidates
.map((candidate) => {
@@ -437,21 +491,34 @@ export function resolveModelDirectiveSelection(params: {
return Object.assign({ candidate }, details);
})
.toSorted((a, b) => {
if (b.score !== a.score) return b.score - a.score;
if (a.isDefault !== b.isDefault) return a.isDefault ? -1 : 1;
if (a.variantMatchCount !== b.variantMatchCount)
if (b.score !== a.score) {
return b.score - a.score;
}
if (a.isDefault !== b.isDefault) {
return a.isDefault ? -1 : 1;
}
if (a.variantMatchCount !== b.variantMatchCount) {
return b.variantMatchCount - a.variantMatchCount;
if (a.variantCount !== b.variantCount) return a.variantCount - b.variantCount;
if (a.modelLength !== b.modelLength) return a.modelLength - b.modelLength;
}
if (a.variantCount !== b.variantCount) {
return a.variantCount - b.variantCount;
}
if (a.modelLength !== b.modelLength) {
return a.modelLength - b.modelLength;
}
return a.key.localeCompare(b.key);
});
const bestScored = scored[0];
const best = bestScored?.candidate;
if (!best || !bestScored) return {};
if (!best || !bestScored) {
return {};
}
const minScore = providerFilter ? 90 : 120;
if (bestScored.score < minScore) return {};
if (bestScored.score < minScore) {
return {};
}
return { selection: buildSelection(best.provider, best.model) };
};
@@ -464,7 +531,9 @@ export function resolveModelDirectiveSelection(params: {
if (!resolved) {
const fuzzy = resolveFuzzy({ fragment: rawTrimmed });
if (fuzzy.selection || fuzzy.error) return fuzzy;
if (fuzzy.selection || fuzzy.error) {
return fuzzy;
}
return {
error: `Unrecognized model "${rawTrimmed}". Use /models to list providers, or /models <provider> to list models.`,
};
@@ -489,12 +558,16 @@ export function resolveModelDirectiveSelection(params: {
const provider = normalizeProviderId(rawTrimmed.slice(0, slash).trim());
const fragment = rawTrimmed.slice(slash + 1).trim();
const fuzzy = resolveFuzzy({ provider, fragment });
if (fuzzy.selection || fuzzy.error) return fuzzy;
if (fuzzy.selection || fuzzy.error) {
return fuzzy;
}
}
// Otherwise, try fuzzy matching across allowlisted models.
const fuzzy = resolveFuzzy({ fragment: rawTrimmed });
if (fuzzy.selection || fuzzy.error) return fuzzy;
if (fuzzy.selection || fuzzy.error) {
return fuzzy;
}
return {
error: `Model "${resolved.ref.provider}/${resolved.ref.model}" is not allowed. Use /models to list providers, or /models <provider> to list models.`,

View File

@@ -51,7 +51,9 @@ export function normalizeReplyPayload(
const shouldStripHeartbeat = opts.stripHeartbeat ?? true;
if (shouldStripHeartbeat && text?.includes(HEARTBEAT_TOKEN)) {
const stripped = stripHeartbeatToken(text, { mode: "message" });
if (stripped.didStrip) opts.onHeartbeatStrip?.();
if (stripped.didStrip) {
opts.onHeartbeatStrip?.();
}
if (stripped.shouldSkip && !hasMedia && !hasChannelData) {
opts.onSkip?.("heartbeat");
return null;

View File

@@ -16,7 +16,9 @@ export function clearSessionQueues(keys: Array<string | undefined>): ClearSessio
for (const key of keys) {
const cleaned = key?.trim();
if (!cleaned || seen.has(cleaned)) continue;
if (!cleaned || seen.has(cleaned)) {
continue;
}
seen.add(cleaned);
clearedKeys.push(cleaned);
followupCleared += clearFollowupQueue(cleaned);

View File

@@ -3,10 +3,14 @@ import { normalizeQueueDropPolicy, normalizeQueueMode } from "./normalize.js";
import type { QueueDropPolicy, QueueMode } from "./types.js";
function parseQueueDebounce(raw?: string): number | undefined {
if (!raw) return undefined;
if (!raw) {
return undefined;
}
try {
const parsed = parseDurationMs(raw.trim(), { defaultUnit: "ms" });
if (!parsed || parsed < 0) return undefined;
if (!parsed || parsed < 0) {
return undefined;
}
return Math.round(parsed);
} catch {
return undefined;
@@ -14,11 +18,17 @@ function parseQueueDebounce(raw?: string): number | undefined {
}
function parseQueueCap(raw?: string): number | undefined {
if (!raw) return undefined;
if (!raw) {
return undefined;
}
const num = Number(raw);
if (!Number.isFinite(num)) return undefined;
if (!Number.isFinite(num)) {
return undefined;
}
const cap = Math.floor(num);
if (cap < 1) return undefined;
if (cap < 1) {
return undefined;
}
return cap;
}
@@ -37,10 +47,14 @@ function parseQueueDirectiveArgs(raw: string): {
} {
let i = 0;
const len = raw.length;
while (i < len && /\s/.test(raw[i])) i += 1;
while (i < len && /\s/.test(raw[i])) {
i += 1;
}
if (raw[i] === ":") {
i += 1;
while (i < len && /\s/.test(raw[i])) i += 1;
while (i < len && /\s/.test(raw[i])) {
i += 1;
}
}
let consumed = i;
let queueMode: QueueMode | undefined;
@@ -54,17 +68,27 @@ function parseQueueDirectiveArgs(raw: string): {
let rawDrop: string | undefined;
let hasOptions = false;
const takeToken = (): string | null => {
if (i >= len) return null;
if (i >= len) {
return null;
}
const start = i;
while (i < len && !/\s/.test(raw[i])) i += 1;
if (start === i) return null;
while (i < len && !/\s/.test(raw[i])) {
i += 1;
}
if (start === i) {
return null;
}
const token = raw.slice(start, i);
while (i < len && /\s/.test(raw[i])) i += 1;
while (i < len && /\s/.test(raw[i])) {
i += 1;
}
return token;
};
while (i < len) {
const token = takeToken();
if (!token) break;
if (!token) {
break;
}
const lowered = token.trim().toLowerCase();
if (lowered === "default" || lowered === "reset" || lowered === "clear") {
queueReset = true;

View File

@@ -14,7 +14,9 @@ export function scheduleFollowupDrain(
runFollowup: (run: FollowupRun) => Promise<void>,
): void {
const queue = FOLLOWUP_QUEUES.get(key);
if (!queue || queue.draining) return;
if (!queue || queue.draining) {
return;
}
queue.draining = true;
void (async () => {
try {
@@ -28,7 +30,9 @@ export function scheduleFollowupDrain(
// Debug: `pnpm test src/auto-reply/reply/queue.collect-routing.test.ts`
if (forceIndividualCollect) {
const next = queue.items.shift();
if (!next) break;
if (!next) {
break;
}
await runFollowup(next);
continue;
}
@@ -55,7 +59,9 @@ export function scheduleFollowupDrain(
if (isCrossChannel) {
forceIndividualCollect = true;
const next = queue.items.shift();
if (!next) break;
if (!next) {
break;
}
await runFollowup(next);
continue;
}
@@ -63,7 +69,9 @@ export function scheduleFollowupDrain(
const items = queue.items.splice(0, queue.items.length);
const summary = buildQueueSummaryPrompt({ state: queue, noun: "message" });
const run = items.at(-1)?.run ?? queue.lastRun;
if (!run) break;
if (!run) {
break;
}
// Preserve originating channel from items when collecting same-channel.
const originatingChannel = items.find((i) => i.originatingChannel)?.originatingChannel;
@@ -96,7 +104,9 @@ export function scheduleFollowupDrain(
const summaryPrompt = buildQueueSummaryPrompt({ state: queue, noun: "message" });
if (summaryPrompt) {
const run = queue.lastRun;
if (!run) break;
if (!run) {
break;
}
await runFollowup({
prompt: summaryPrompt,
run,
@@ -106,7 +116,9 @@ export function scheduleFollowupDrain(
}
const next = queue.items.shift();
if (!next) break;
if (!next) {
break;
}
await runFollowup(next);
}
} catch (err) {

View File

@@ -17,7 +17,9 @@ function isRunAlreadyQueued(
if (messageId) {
return items.some((item) => item.messageId?.trim() === messageId && hasSameRouting(item));
}
if (!allowPromptFallback) return false;
if (!allowPromptFallback) {
return false;
}
return items.some((item) => item.prompt === run.prompt && hasSameRouting(item));
}
@@ -35,7 +37,9 @@ export function enqueueFollowupRun(
isRunAlreadyQueued(item, items, dedupeMode === "prompt");
// Deduplicate: skip if the same message is already queued.
if (shouldSkipQueueItem({ item: run, items: queue.items, dedupe })) return false;
if (shouldSkipQueueItem({ item: run, items: queue.items, dedupe })) {
return false;
}
queue.lastEnqueuedAt = Date.now();
queue.lastRun = run.run;
@@ -44,7 +48,9 @@ export function enqueueFollowupRun(
queue,
summarize: (item) => item.summaryLine?.trim() || item.prompt.trim(),
});
if (!shouldEnqueue) return false;
if (!shouldEnqueue) {
return false;
}
queue.items.push(run);
return true;
@@ -52,8 +58,12 @@ export function enqueueFollowupRun(
export function getFollowupQueueDepth(key: string): number {
const cleaned = key.trim();
if (!cleaned) return 0;
if (!cleaned) {
return 0;
}
const queue = FOLLOWUP_QUEUES.get(cleaned);
if (!queue) return 0;
if (!queue) {
return 0;
}
return queue.items.length;
}

View File

@@ -1,25 +1,44 @@
import type { QueueDropPolicy, QueueMode } from "./types.js";
export function normalizeQueueMode(raw?: string): QueueMode | undefined {
if (!raw) return undefined;
if (!raw) {
return undefined;
}
const cleaned = raw.trim().toLowerCase();
if (cleaned === "queue" || cleaned === "queued") return "steer";
if (cleaned === "interrupt" || cleaned === "interrupts" || cleaned === "abort")
if (cleaned === "queue" || cleaned === "queued") {
return "steer";
}
if (cleaned === "interrupt" || cleaned === "interrupts" || cleaned === "abort") {
return "interrupt";
if (cleaned === "steer" || cleaned === "steering") return "steer";
if (cleaned === "followup" || cleaned === "follow-ups" || cleaned === "followups")
}
if (cleaned === "steer" || cleaned === "steering") {
return "steer";
}
if (cleaned === "followup" || cleaned === "follow-ups" || cleaned === "followups") {
return "followup";
if (cleaned === "collect" || cleaned === "coalesce") return "collect";
if (cleaned === "steer+backlog" || cleaned === "steer-backlog" || cleaned === "steer_backlog")
}
if (cleaned === "collect" || cleaned === "coalesce") {
return "collect";
}
if (cleaned === "steer+backlog" || cleaned === "steer-backlog" || cleaned === "steer_backlog") {
return "steer-backlog";
}
return undefined;
}
export function normalizeQueueDropPolicy(raw?: string): QueueDropPolicy | undefined {
if (!raw) return undefined;
if (!raw) {
return undefined;
}
const cleaned = raw.trim().toLowerCase();
if (cleaned === "old" || cleaned === "oldest") return "old";
if (cleaned === "new" || cleaned === "newest") return "new";
if (cleaned === "summarize" || cleaned === "summary") return "summarize";
if (cleaned === "old" || cleaned === "oldest") {
return "old";
}
if (cleaned === "new" || cleaned === "newest") {
return "new";
}
if (cleaned === "summarize" || cleaned === "summary") {
return "summarize";
}
return undefined;
}

View File

@@ -13,13 +13,17 @@ function resolveChannelDebounce(
byChannel: InboundDebounceByProvider | undefined,
channelKey: string | undefined,
): number | undefined {
if (!channelKey || !byChannel) return undefined;
if (!channelKey || !byChannel) {
return undefined;
}
const value = byChannel[channelKey];
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : undefined;
}
function resolvePluginDebounce(channelKey: string | undefined): number | undefined {
if (!channelKey) return undefined;
if (!channelKey) {
return undefined;
}
const plugin = getChannelPlugin(channelKey);
const value = plugin?.defaults?.queue?.debounceMs;
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, value) : undefined;

View File

@@ -58,9 +58,13 @@ export function getFollowupQueue(key: string, settings: QueueSettings): Followup
export function clearFollowupQueue(key: string): number {
const cleaned = key.trim();
if (!cleaned) return 0;
if (!cleaned) {
return 0;
}
const queue = FOLLOWUP_QUEUES.get(cleaned);
if (!queue) return 0;
if (!queue) {
return 0;
}
const cleared = queue.items.length + queue.droppedCount;
queue.items.length = 0;
queue.droppedCount = 0;

View File

@@ -24,12 +24,16 @@ const DEFAULT_HUMAN_DELAY_MAX_MS = 2500;
/** Generate a random delay within the configured range. */
function getHumanDelay(config: HumanDelayConfig | undefined): number {
const mode = config?.mode ?? "off";
if (mode === "off") return 0;
if (mode === "off") {
return 0;
}
const min =
mode === "custom" ? (config?.minMs ?? DEFAULT_HUMAN_DELAY_MIN_MS) : DEFAULT_HUMAN_DELAY_MIN_MS;
const max =
mode === "custom" ? (config?.maxMs ?? DEFAULT_HUMAN_DELAY_MAX_MS) : DEFAULT_HUMAN_DELAY_MAX_MS;
if (max <= min) return min;
if (max <= min) {
return min;
}
return Math.floor(Math.random() * (max - min + 1)) + min;
}
@@ -115,20 +119,26 @@ export function createReplyDispatcher(options: ReplyDispatcherOptions): ReplyDis
onHeartbeatStrip: options.onHeartbeatStrip,
onSkip: (reason) => options.onSkip?.(payload, { kind, reason }),
});
if (!normalized) return false;
if (!normalized) {
return false;
}
queuedCounts[kind] += 1;
pending += 1;
// Determine if we should add human-like delay (only for block replies after the first).
const shouldDelay = kind === "block" && sentFirstBlock;
if (kind === "block") sentFirstBlock = true;
if (kind === "block") {
sentFirstBlock = true;
}
sendChain = sendChain
.then(async () => {
// Add human-like delay between block replies for natural rhythm.
if (shouldDelay) {
const delayMs = getHumanDelay(options.humanDelay);
if (delayMs > 0) await sleep(delayMs);
if (delayMs > 0) {
await sleep(delayMs);
}
}
await options.deliver(normalized, { kind });
})

View File

@@ -8,14 +8,20 @@ import { formatCliCommand } from "../../cli/command-format.js";
import type { MsgContext } from "../templating.js";
function normalizeAllowToken(value?: string) {
if (!value) return "";
if (!value) {
return "";
}
return value.trim().toLowerCase();
}
function slugAllowToken(value?: string) {
if (!value) return "";
if (!value) {
return "";
}
let text = value.trim().toLowerCase();
if (!text) return "";
if (!text) {
return "";
}
text = text.replace(/^[@#]+/, "");
text = text.replace(/[\s_]+/g, "-");
text = text.replace(/[^a-z0-9-]+/g, "-");
@@ -32,7 +38,9 @@ const SENDER_PREFIXES = [
const SENDER_PREFIX_RE = new RegExp(`^(${SENDER_PREFIXES.join("|")}):`, "i");
function stripSenderPrefix(value?: string) {
if (!value) return "";
if (!value) {
return "";
}
const trimmed = value.trim();
return trimmed.replace(SENDER_PREFIX_RE, "");
}
@@ -42,7 +50,9 @@ function resolveElevatedAllowList(
provider: string,
fallbackAllowFrom?: Array<string | number>,
): Array<string | number> | undefined {
if (!allowFrom) return fallbackAllowFrom;
if (!allowFrom) {
return fallbackAllowFrom;
}
const value = allowFrom[provider];
return Array.isArray(value) ? value : fallbackAllowFrom;
}
@@ -58,22 +68,36 @@ function isApprovedElevatedSender(params: {
params.provider,
params.fallbackAllowFrom,
);
if (!rawAllow || rawAllow.length === 0) return false;
if (!rawAllow || rawAllow.length === 0) {
return false;
}
const allowTokens = rawAllow.map((entry) => String(entry).trim()).filter(Boolean);
if (allowTokens.length === 0) return false;
if (allowTokens.some((entry) => entry === "*")) return true;
if (allowTokens.length === 0) {
return false;
}
if (allowTokens.some((entry) => entry === "*")) {
return true;
}
const tokens = new Set<string>();
const addToken = (value?: string) => {
if (!value) return;
if (!value) {
return;
}
const trimmed = value.trim();
if (!trimmed) return;
if (!trimmed) {
return;
}
tokens.add(trimmed);
const normalized = normalizeAllowToken(trimmed);
if (normalized) tokens.add(normalized);
if (normalized) {
tokens.add(normalized);
}
const slugged = slugAllowToken(trimmed);
if (slugged) tokens.add(slugged);
if (slugged) {
tokens.add(slugged);
}
};
addToken(params.ctx.SenderName);
@@ -87,13 +111,21 @@ function isApprovedElevatedSender(params: {
for (const rawEntry of allowTokens) {
const entry = rawEntry.trim();
if (!entry) continue;
if (!entry) {
continue;
}
const stripped = stripSenderPrefix(entry);
if (tokens.has(entry) || tokens.has(stripped)) return true;
if (tokens.has(entry) || tokens.has(stripped)) {
return true;
}
const normalized = normalizeAllowToken(stripped);
if (normalized && tokens.has(normalized)) return true;
if (normalized && tokens.has(normalized)) {
return true;
}
const slugged = slugAllowToken(stripped);
if (slugged && tokens.has(slugged)) return true;
if (slugged && tokens.has(slugged)) {
return true;
}
}
return false;
@@ -115,13 +147,18 @@ export function resolveElevatedPermissions(params: {
const agentEnabled = agentConfig?.enabled !== false;
const enabled = globalEnabled && agentEnabled;
const failures: Array<{ gate: string; key: string }> = [];
if (!globalEnabled) failures.push({ gate: "enabled", key: "tools.elevated.enabled" });
if (!agentEnabled)
if (!globalEnabled) {
failures.push({ gate: "enabled", key: "tools.elevated.enabled" });
}
if (!agentEnabled) {
failures.push({
gate: "enabled",
key: "agents.list[].tools.elevated.enabled",
});
if (!enabled) return { enabled, allowed: false, failures };
}
if (!enabled) {
return { enabled, allowed: false, failures };
}
if (!params.provider) {
failures.push({ gate: "provider", key: "ctx.Provider" });
return { enabled, allowed: false, failures };

View File

@@ -12,12 +12,18 @@ export function extractInlineSimpleCommand(body?: string): {
command: string;
cleaned: string;
} | null {
if (!body) return null;
if (!body) {
return null;
}
const match = body.match(INLINE_SIMPLE_COMMAND_RE);
if (!match || match.index === undefined) return null;
if (!match || match.index === undefined) {
return null;
}
const alias = `/${match[1].toLowerCase()}`;
const command = INLINE_SIMPLE_COMMAND_ALIASES.get(alias);
if (!command) return null;
if (!command) {
return null;
}
const cleaned = body.replace(match[0], " ").replace(/\s+/g, " ").trim();
return { command, cleaned };
}
@@ -27,7 +33,9 @@ export function stripInlineStatus(body: string): {
didStrip: boolean;
} {
const trimmed = body.trim();
if (!trimmed) return { cleaned: "", didStrip: false };
if (!trimmed) {
return { cleaned: "", didStrip: false };
}
const cleaned = trimmed.replace(INLINE_STATUS_RE, " ").replace(/\s+/g, " ").trim();
return { cleaned, didStrip: cleaned !== trimmed };
}

View File

@@ -12,7 +12,9 @@ export function applyReplyTagsToPayload(
currentMessageId?: string,
): ReplyPayload {
if (typeof payload.text !== "string") {
if (!payload.replyToCurrent || payload.replyToId) return payload;
if (!payload.replyToCurrent || payload.replyToId) {
return payload;
}
return {
...payload,
replyToId: currentMessageId?.trim() || undefined,
@@ -20,7 +22,9 @@ export function applyReplyTagsToPayload(
}
const shouldParseTags = payload.text.includes("[[");
if (!shouldParseTags) {
if (!payload.replyToCurrent || payload.replyToId) return payload;
if (!payload.replyToCurrent || payload.replyToId) {
return payload;
}
return {
...payload,
replyToId: currentMessageId?.trim() || undefined,
@@ -69,7 +73,9 @@ export function filterMessagingToolDuplicates(params: {
sentTexts: string[];
}): ReplyPayload[] {
const { payloads, sentTexts } = params;
if (sentTexts.length === 0) return payloads;
if (sentTexts.length === 0) {
return payloads;
}
return payloads.filter((payload) => !isMessagingToolDuplicate(payload.text ?? "", sentTexts));
}
@@ -85,17 +91,29 @@ export function shouldSuppressMessagingToolReplies(params: {
accountId?: string;
}): boolean {
const provider = params.messageProvider?.trim().toLowerCase();
if (!provider) return false;
if (!provider) {
return false;
}
const originTarget = normalizeTargetForProvider(provider, params.originatingTo);
if (!originTarget) return false;
if (!originTarget) {
return false;
}
const originAccount = normalizeAccountId(params.accountId);
const sentTargets = params.messagingToolSentTargets ?? [];
if (sentTargets.length === 0) return false;
if (sentTargets.length === 0) {
return false;
}
return sentTargets.some((target) => {
if (!target?.provider) return false;
if (target.provider.trim().toLowerCase() !== provider) return false;
if (!target?.provider) {
return false;
}
if (target.provider.trim().toLowerCase() !== provider) {
return false;
}
const targetKey = normalizeTargetForProvider(provider, target.to);
if (!targetKey) return false;
if (!targetKey) {
return false;
}
const targetAccount = normalizeAccountId(target.accountId);
if (originAccount && targetAccount && originAccount !== targetAccount) {
return false;

View File

@@ -26,13 +26,19 @@ export function createReplyReferencePlanner(options: {
const startId = options.startId?.trim();
const use = (): string | undefined => {
if (!allowReference) return undefined;
if (!allowReference) {
return undefined;
}
if (existingId) {
hasReplied = true;
return existingId;
}
if (!startId) return undefined;
if (options.replyToMode === "off") return undefined;
if (!startId) {
return undefined;
}
if (options.replyToMode === "off") {
return undefined;
}
if (options.replyToMode === "all") {
hasReplied = true;
return startId;

View File

@@ -12,7 +12,9 @@ export function resolveReplyToMode(
chatType?: string | null,
): ReplyToMode {
const provider = normalizeChannelId(channel);
if (!provider) return "all";
if (!provider) {
return "all";
}
const resolved = getChannelDock(provider)?.threading?.resolveReplyToMode?.({
cfg,
accountId,
@@ -27,12 +29,18 @@ export function createReplyToModeFilter(
) {
let hasThreaded = false;
return (payload: ReplyPayload): ReplyPayload => {
if (!payload.replyToId) return payload;
if (!payload.replyToId) {
return payload;
}
if (mode === "off") {
if (opts.allowTagsWhenOff && payload.replyToTag) return payload;
if (opts.allowTagsWhenOff && payload.replyToTag) {
return payload;
}
return { ...payload, replyToId: undefined };
}
if (mode === "all") return payload;
if (mode === "all") {
return payload;
}
if (hasThreaded) {
return { ...payload, replyToId: undefined };
}

View File

@@ -39,7 +39,9 @@ export function resolveResponsePrefixTemplate(
template: string | undefined,
context: ResponsePrefixContext,
): string | undefined {
if (!template) return undefined;
if (!template) {
return undefined;
}
return template.replace(TEMPLATE_VAR_PATTERN, (match, varName: string) => {
const normalizedVar = varName.toLowerCase();
@@ -90,7 +92,9 @@ export function extractShortModelName(fullModel: string): string {
* Check if a template string contains any template variables.
*/
export function hasTemplateVariables(template: string | undefined): boolean {
if (!template) return false;
if (!template) {
return false;
}
// Reset lastIndex since we're using a global regex
TEMPLATE_VAR_PATTERN.lastIndex = 0;
return TEMPLATE_VAR_PATTERN.test(template);

View File

@@ -72,7 +72,9 @@ export async function routeReply(params: RouteReplyParams): Promise<RouteReplyRe
const normalized = normalizeReplyPayload(payload, {
responsePrefix,
});
if (!normalized) return { ok: true };
if (!normalized) {
return { ok: true };
}
let text = normalized.text ?? "";
let mediaUrls = (normalized.mediaUrls?.filter(Boolean) ?? []).length
@@ -151,6 +153,8 @@ export async function routeReply(params: RouteReplyParams): Promise<RouteReplyRe
export function isRoutableChannel(
channel: OriginatingChannelType | undefined,
): channel is Exclude<OriginatingChannelType, typeof INTERNAL_MESSAGE_CHANNEL> {
if (!channel || channel === INTERNAL_MESSAGE_CHANNEL) return false;
if (!channel || channel === INTERNAL_MESSAGE_CHANNEL) {
return false;
}
return normalizeChannelId(channel) !== null;
}

View File

@@ -41,9 +41,13 @@ function buildSelectionFromExplicit(params: {
defaultProvider: params.defaultProvider,
aliasIndex: params.aliasIndex,
});
if (!resolved) return undefined;
if (!resolved) {
return undefined;
}
const key = modelKey(resolved.ref.provider, resolved.ref.model);
if (params.allowedModelKeys.size > 0 && !params.allowedModelKeys.has(key)) return undefined;
if (params.allowedModelKeys.size > 0 && !params.allowedModelKeys.has(key)) {
return undefined;
}
const isDefault =
resolved.ref.provider === params.defaultProvider && resolved.ref.model === params.defaultModel;
return {
@@ -62,12 +66,16 @@ function applySelectionToSession(params: {
storePath?: string;
}) {
const { selection, sessionEntry, sessionStore, sessionKey, storePath } = params;
if (!sessionEntry || !sessionStore || !sessionKey) return;
if (!sessionEntry || !sessionStore || !sessionKey) {
return;
}
const { updated } = applyModelOverrideToSessionEntry({
entry: sessionEntry,
selection,
});
if (!updated) return;
if (!updated) {
return;
}
sessionStore[sessionKey] = sessionEntry;
if (storePath) {
updateSessionStore(storePath, (store) => {
@@ -92,12 +100,18 @@ export async function applyResetModelOverride(params: {
defaultModel: string;
aliasIndex: ModelAliasIndex;
}): Promise<ResetModelResult> {
if (!params.resetTriggered) return {};
if (!params.resetTriggered) {
return {};
}
const rawBody = params.bodyStripped?.trim();
if (!rawBody) return {};
if (!rawBody) {
return {};
}
const { tokens, first, second } = splitBody(rawBody);
if (!first) return {};
if (!first) {
return {};
}
const catalog = await loadModelCatalog({ config: params.cfg });
const allowed = buildAllowedModelSet({
@@ -107,12 +121,16 @@ export async function applyResetModelOverride(params: {
defaultModel: params.defaultModel,
});
const allowedModelKeys = allowed.allowedKeys;
if (allowedModelKeys.size === 0) return {};
if (allowedModelKeys.size === 0) {
return {};
}
const providers = new Set<string>();
for (const key of allowedModelKeys) {
const slash = key.indexOf("/");
if (slash <= 0) continue;
if (slash <= 0) {
continue;
}
providers.add(normalizeProviderId(key.slice(0, slash)));
}
@@ -145,7 +163,9 @@ export async function applyResetModelOverride(params: {
aliasIndex: params.aliasIndex,
allowedModelKeys,
});
if (selection) consumed = 1;
if (selection) {
consumed = 1;
}
}
if (!selection) {
@@ -153,11 +173,15 @@ export async function applyResetModelOverride(params: {
const allowFuzzy = providers.has(normalizeProviderId(first)) || first.trim().length >= 6;
if (allowFuzzy) {
selection = resolved.selection;
if (selection) consumed = 1;
if (selection) {
consumed = 1;
}
}
}
if (!selection) return {};
if (!selection) {
return {};
}
const cleanedBody = tokens.slice(consumed).join(" ").trim();
params.sessionCtx.BodyStripped = formatInboundBodyWithSenderMeta({

View File

@@ -18,14 +18,22 @@ export async function prependSystemEvents(params: {
}): Promise<string> {
const compactSystemEvent = (line: string): string | null => {
const trimmed = line.trim();
if (!trimmed) return null;
if (!trimmed) {
return null;
}
const lower = trimmed.toLowerCase();
if (lower.includes("reason periodic")) return null;
if (lower.includes("reason periodic")) {
return null;
}
// Filter out the actual heartbeat prompt, but not cron jobs that mention "heartbeat"
// The heartbeat prompt starts with "Read HEARTBEAT.md" - cron payloads won't match this
if (lower.startsWith("read heartbeat.md")) return null;
if (lower.startsWith("read heartbeat.md")) {
return null;
}
// Also filter heartbeat poll/wake noise
if (lower.includes("heartbeat poll") || lower.includes("heartbeat wake")) return null;
if (lower.includes("heartbeat poll") || lower.includes("heartbeat wake")) {
return null;
}
if (trimmed.startsWith("Node:")) {
return trimmed.replace(/ · last input [^·]+/i, "").trim();
}
@@ -43,10 +51,16 @@ export async function prependSystemEvents(params: {
const resolveSystemEventTimezone = (cfg: OpenClawConfig) => {
const raw = cfg.agents?.defaults?.envelopeTimezone?.trim();
if (!raw) return { mode: "local" as const };
if (!raw) {
return { mode: "local" as const };
}
const lowered = raw.toLowerCase();
if (lowered === "utc" || lowered === "gmt") return { mode: "utc" as const };
if (lowered === "local" || lowered === "host") return { mode: "local" as const };
if (lowered === "utc" || lowered === "gmt") {
return { mode: "utc" as const };
}
if (lowered === "local" || lowered === "host") {
return { mode: "local" as const };
}
if (lowered === "user") {
return {
mode: "iana" as const,
@@ -90,16 +104,24 @@ export async function prependSystemEvents(params: {
.toReversed()
.find((part) => part.type === "timeZoneName")
?.value?.trim();
if (!yyyy || !mm || !dd || !hh || !min || !sec) return undefined;
if (!yyyy || !mm || !dd || !hh || !min || !sec) {
return undefined;
}
return `${yyyy}-${mm}-${dd} ${hh}:${min}:${sec}${tz ? ` ${tz}` : ""}`;
};
const formatSystemEventTimestamp = (ts: number, cfg: OpenClawConfig) => {
const date = new Date(ts);
if (Number.isNaN(date.getTime())) return "unknown-time";
if (Number.isNaN(date.getTime())) {
return "unknown-time";
}
const zone = resolveSystemEventTimezone(cfg);
if (zone.mode === "utc") return formatUtcTimestamp(date);
if (zone.mode === "local") return formatZonedTimestamp(date) ?? "unknown-time";
if (zone.mode === "utc") {
return formatUtcTimestamp(date);
}
if (zone.mode === "local") {
return formatZonedTimestamp(date) ?? "unknown-time";
}
return formatZonedTimestamp(date, zone.timeZone) ?? "unknown-time";
};
@@ -109,16 +131,22 @@ export async function prependSystemEvents(params: {
...queued
.map((event) => {
const compacted = compactSystemEvent(event.text);
if (!compacted) return null;
if (!compacted) {
return null;
}
return `[${formatSystemEventTimestamp(event.ts, params.cfg)}] ${compacted}`;
})
.filter((v): v is string => Boolean(v)),
);
if (params.isMainSession && params.isNewSession) {
const summary = await buildChannelSummary(params.cfg);
if (summary.length > 0) systemLines.unshift(...summary);
if (summary.length > 0) {
systemLines.unshift(...summary);
}
}
if (systemLines.length === 0) {
return params.prefixedBodyBase;
}
if (systemLines.length === 0) return params.prefixedBodyBase;
const block = systemLines.map((l) => `System: ${l}`).join("\n");
return `${block}\n\n${params.prefixedBodyBase}`;
@@ -252,9 +280,13 @@ export async function incrementCompactionCount(params: {
now = Date.now(),
tokensAfter,
} = params;
if (!sessionStore || !sessionKey) return undefined;
if (!sessionStore || !sessionKey) {
return undefined;
}
const entry = sessionStore[sessionKey] ?? sessionEntry;
if (!entry) return undefined;
if (!entry) {
return undefined;
}
const nextCount = (entry.compactionCount ?? 0) + 1;
// Build update payload with compaction count and optionally updated token counts
const updates: Partial<SessionEntry> = {

View File

@@ -19,7 +19,9 @@ export async function persistSessionUsageUpdate(params: {
logLabel?: string;
}): Promise<void> {
const { storePath, sessionKey } = params;
if (!storePath || !sessionKey) return;
if (!storePath || !sessionKey) {
return;
}
const label = params.logLabel ? `${params.logLabel} ` : "";
if (hasNonzeroUsage(params.usage)) {

View File

@@ -60,14 +60,18 @@ function forkSessionFromParent(params: {
params.parentEntry.sessionId,
params.parentEntry,
);
if (!parentSessionFile || !fs.existsSync(parentSessionFile)) return null;
if (!parentSessionFile || !fs.existsSync(parentSessionFile)) {
return null;
}
try {
const manager = SessionManager.open(parentSessionFile);
const leafId = manager.getLeafId();
if (leafId) {
const sessionFile = manager.createBranchedSession(leafId) ?? manager.getSessionFile();
const sessionId = manager.getSessionId();
if (sessionFile && sessionId) return { sessionId, sessionFile };
if (sessionFile && sessionId) {
return { sessionId, sessionFile };
}
}
const sessionId = crypto.randomUUID();
const timestamp = new Date().toISOString();
@@ -165,8 +169,12 @@ export async function initSessionState(params: {
const strippedForResetLower = strippedForReset.toLowerCase();
for (const trigger of resetTriggers) {
if (!trigger) continue;
if (!resetAuthorized) break;
if (!trigger) {
continue;
}
if (!resetAuthorized) {
break;
}
const triggerLower = trigger.toLowerCase();
if (trimmedBodyLower === triggerLower || strippedForResetLower === triggerLower) {
isNewSession = true;

View File

@@ -24,7 +24,9 @@ export async function stageSandboxMedia(params: {
: ctx.MediaPath?.trim()
? [ctx.MediaPath.trim()]
: [];
if (rawPaths.length === 0 || !sessionKey) return;
if (rawPaths.length === 0 || !sessionKey) {
return;
}
const sandbox = await ensureSandboxWorkspaceForSession({
config: cfg,
@@ -37,11 +39,15 @@ export async function stageSandboxMedia(params: {
? path.join(CONFIG_DIR, "media", "remote-cache", sessionKey)
: null;
const effectiveWorkspaceDir = sandbox?.workspaceDir ?? remoteMediaCacheDir;
if (!effectiveWorkspaceDir) return;
if (!effectiveWorkspaceDir) {
return;
}
const resolveAbsolutePath = (value: string): string | null => {
let resolved = value.trim();
if (!resolved) return null;
if (!resolved) {
return null;
}
if (resolved.startsWith("file://")) {
try {
resolved = fileURLToPath(resolved);
@@ -49,7 +55,9 @@ export async function stageSandboxMedia(params: {
return null;
}
}
if (!path.isAbsolute(resolved)) return null;
if (!path.isAbsolute(resolved)) {
return null;
}
return resolved;
};
@@ -65,11 +73,17 @@ export async function stageSandboxMedia(params: {
for (const raw of rawPaths) {
const source = resolveAbsolutePath(raw);
if (!source) continue;
if (staged.has(source)) continue;
if (!source) {
continue;
}
if (staged.has(source)) {
continue;
}
const baseName = path.basename(source);
if (!baseName) continue;
if (!baseName) {
continue;
}
const parsed = path.parse(baseName);
let fileName = baseName;
let suffix = 1;
@@ -93,9 +107,13 @@ export async function stageSandboxMedia(params: {
const rewriteIfStaged = (value: string | undefined): string | undefined => {
const raw = value?.trim();
if (!raw) return value;
if (!raw) {
return value;
}
const abs = resolveAbsolutePath(raw);
if (!abs) return value;
if (!abs) {
return value;
}
const mapped = staged.get(abs);
return mapped ?? value;
};
@@ -152,8 +170,11 @@ async function scpFile(remoteHost: string, remotePath: string, localPath: string
child.once("error", reject);
child.once("exit", (code) => {
if (code === 0) resolve();
else reject(new Error(`scp failed (${code}): ${stderr.trim()}`));
if (code === 0) {
resolve();
} else {
reject(new Error(`scp failed (${code}): ${stderr.trim()}`));
}
});
});
}

View File

@@ -20,9 +20,13 @@ type ConsumeOptions = {
const splitTrailingDirective = (text: string): { text: string; tail: string } => {
const openIndex = text.lastIndexOf("[[");
if (openIndex < 0) return { text, tail: "" };
if (openIndex < 0) {
return { text, tail: "" };
}
const closeIndex = text.indexOf("]]", openIndex + 2);
if (closeIndex >= 0) return { text, tail: "" };
if (closeIndex >= 0) {
return { text, tail: "" };
}
return {
text: text.slice(0, openIndex),
tail: text.slice(openIndex),

View File

@@ -2,23 +2,37 @@ import type { SubagentRunRecord } from "../../agents/subagent-registry.js";
import { truncateUtf16Safe } from "../../utils.js";
export function formatDurationShort(valueMs?: number) {
if (!valueMs || !Number.isFinite(valueMs) || valueMs <= 0) return "n/a";
if (!valueMs || !Number.isFinite(valueMs) || valueMs <= 0) {
return "n/a";
}
const totalSeconds = Math.round(valueMs / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
if (hours > 0) return `${hours}h${minutes}m`;
if (minutes > 0) return `${minutes}m${seconds}s`;
if (hours > 0) {
return `${hours}h${minutes}m`;
}
if (minutes > 0) {
return `${minutes}m${seconds}s`;
}
return `${seconds}s`;
}
export function formatAgeShort(valueMs?: number) {
if (!valueMs || !Number.isFinite(valueMs) || valueMs <= 0) return "n/a";
if (!valueMs || !Number.isFinite(valueMs) || valueMs <= 0) {
return "n/a";
}
const minutes = Math.round(valueMs / 60_000);
if (minutes < 1) return "just now";
if (minutes < 60) return `${minutes}m ago`;
if (minutes < 1) {
return "just now";
}
if (minutes < 60) {
return `${minutes}m ago`;
}
const hours = Math.round(minutes / 60);
if (hours < 48) return `${hours}h ago`;
if (hours < 48) {
return `${hours}h ago`;
}
const days = Math.round(hours / 24);
return `${days}d ago`;
}
@@ -31,12 +45,16 @@ export function resolveSubagentLabel(entry: SubagentRunRecord, fallback = "subag
export function formatRunLabel(entry: SubagentRunRecord, options?: { maxLength?: number }) {
const raw = resolveSubagentLabel(entry);
const maxLength = options?.maxLength ?? 72;
if (!Number.isFinite(maxLength) || maxLength <= 0) return raw;
if (!Number.isFinite(maxLength) || maxLength <= 0) {
return raw;
}
return raw.length > maxLength ? `${truncateUtf16Safe(raw, maxLength).trimEnd()}` : raw;
}
export function formatRunStatus(entry: SubagentRunRecord) {
if (!entry.endedAt) return "running";
if (!entry.endedAt) {
return "running";
}
const status = entry.outcome?.status ?? "done";
return status === "ok" ? "done" : status;
}

View File

@@ -17,9 +17,15 @@ export function resolveTypingMode({
wasMentioned,
isHeartbeat,
}: TypingModeContext): TypingMode {
if (isHeartbeat) return "never";
if (configured) return configured;
if (!isGroupChat || wasMentioned) return "instant";
if (isHeartbeat) {
return "never";
}
if (configured) {
return configured;
}
if (!isGroupChat || wasMentioned) {
return "instant";
}
return DEFAULT_GROUP_TYPING_MODE;
}
@@ -51,23 +57,33 @@ export function createTypingSignaler(params: {
const isRenderableText = (text?: string): boolean => {
const trimmed = text?.trim();
if (!trimmed) return false;
if (!trimmed) {
return false;
}
return !isSilentReplyText(trimmed, SILENT_REPLY_TOKEN);
};
const signalRunStart = async () => {
if (disabled || !shouldStartImmediately) return;
if (disabled || !shouldStartImmediately) {
return;
}
await typing.startTypingLoop();
};
const signalMessageStart = async () => {
if (disabled || !shouldStartOnMessageStart) return;
if (!hasRenderableText) return;
if (disabled || !shouldStartOnMessageStart) {
return;
}
if (!hasRenderableText) {
return;
}
await typing.startTypingLoop();
};
const signalTextDelta = async (text?: string) => {
if (disabled) return;
if (disabled) {
return;
}
const renderable = isRenderableText(text);
if (renderable) {
hasRenderableText = true;
@@ -87,14 +103,20 @@ export function createTypingSignaler(params: {
};
const signalReasoningDelta = async () => {
if (disabled || !shouldStartOnReasoning) return;
if (!hasRenderableText) return;
if (disabled || !shouldStartOnReasoning) {
return;
}
if (!hasRenderableText) {
return;
}
await typing.startTypingLoop();
typing.refreshTypingTtl();
};
const signalToolStart = async () => {
if (disabled) return;
if (disabled) {
return;
}
// Start typing as soon as tools begin executing, even before the first text delta.
if (!typing.isActive()) {
await typing.startTypingLoop();

View File

@@ -38,7 +38,9 @@ export function createTypingController(params: {
const typingIntervalMs = typingIntervalSeconds * 1000;
const formatTypingTtl = (ms: number) => {
if (ms % 60_000 === 0) return `${ms / 60_000}m`;
if (ms % 60_000 === 0) {
return `${ms / 60_000}m`;
}
return `${Math.round(ms / 1000)}s`;
};
@@ -50,7 +52,9 @@ export function createTypingController(params: {
};
const cleanup = () => {
if (sealed) return;
if (sealed) {
return;
}
if (typingTtlTimer) {
clearTimeout(typingTtlTimer);
typingTtlTimer = undefined;
@@ -64,14 +68,22 @@ export function createTypingController(params: {
};
const refreshTypingTtl = () => {
if (sealed) return;
if (!typingIntervalMs || typingIntervalMs <= 0) return;
if (typingTtlMs <= 0) return;
if (sealed) {
return;
}
if (!typingIntervalMs || typingIntervalMs <= 0) {
return;
}
if (typingTtlMs <= 0) {
return;
}
if (typingTtlTimer) {
clearTimeout(typingTtlTimer);
}
typingTtlTimer = setTimeout(() => {
if (!typingTimer) return;
if (!typingTimer) {
return;
}
log?.(`typing TTL reached (${formatTypingTtl(typingTtlMs)}); stopping typing indicator`);
cleanup();
}, typingTtlMs);
@@ -80,37 +92,59 @@ export function createTypingController(params: {
const isActive = () => active && !sealed;
const triggerTyping = async () => {
if (sealed) return;
if (sealed) {
return;
}
await onReplyStart?.();
};
const ensureStart = async () => {
if (sealed) return;
if (sealed) {
return;
}
// Late callbacks after a run completed should never restart typing.
if (runComplete) return;
if (runComplete) {
return;
}
if (!active) {
active = true;
}
if (started) return;
if (started) {
return;
}
started = true;
await triggerTyping();
};
const maybeStopOnIdle = () => {
if (!active) return;
if (!active) {
return;
}
// Stop only when the model run is done and the dispatcher queue is empty.
if (runComplete && dispatchIdle) cleanup();
if (runComplete && dispatchIdle) {
cleanup();
}
};
const startTypingLoop = async () => {
if (sealed) return;
if (runComplete) return;
if (sealed) {
return;
}
if (runComplete) {
return;
}
// Always refresh TTL when called, even if loop already running.
// This keeps typing alive during long tool executions.
refreshTypingTtl();
if (!onReplyStart) return;
if (typingIntervalMs <= 0) return;
if (typingTimer) return;
if (!onReplyStart) {
return;
}
if (typingIntervalMs <= 0) {
return;
}
if (typingTimer) {
return;
}
await ensureStart();
typingTimer = setInterval(() => {
void triggerTyping();
@@ -118,10 +152,16 @@ export function createTypingController(params: {
};
const startTypingOnText = async (text?: string) => {
if (sealed) return;
if (sealed) {
return;
}
const trimmed = text?.trim();
if (!trimmed) return;
if (silentToken && isSilentReplyText(trimmed, silentToken)) return;
if (!trimmed) {
return;
}
if (silentToken && isSilentReplyText(trimmed, silentToken)) {
return;
}
refreshTypingTtl();
await startTypingLoop();
};