2025-11-25 02:16:54 +01:00
import crypto from "node:crypto" ;
2026-01-05 06:18:11 +01:00
import fs from "node:fs/promises" ;
import path from "node:path" ;
import { fileURLToPath } from "node:url" ;
2026-01-06 18:25:37 +00:00
import {
resolveAgentDir ,
resolveAgentIdFromSessionKey ,
resolveAgentWorkspaceDir ,
} from "../agents/agent-scope.js" ;
2026-01-04 05:47:21 +01:00
import { resolveModelRefFromString } from "../agents/model-selection.js" ;
2025-12-20 16:10:46 +01:00
import {
2025-12-26 13:35:44 +01:00
abortEmbeddedPiRun ,
2026-01-03 04:26:36 +01:00
isEmbeddedPiRunActive ,
isEmbeddedPiRunStreaming ,
2025-12-26 13:35:44 +01:00
resolveEmbeddedSessionLane ,
2025-12-20 16:10:46 +01:00
} from "../agents/pi-embedded.js" ;
2026-01-05 06:18:11 +01:00
import { ensureSandboxWorkspaceForSession } from "../agents/sandbox.js" ;
2026-01-06 02:48:44 +00:00
import { resolveAgentTimeoutMs } from "../agents/timeout.js" ;
2025-12-14 03:14:51 +00:00
import {
DEFAULT_AGENT_WORKSPACE_DIR ,
ensureAgentWorkspace ,
} from "../agents/workspace.js" ;
2026-01-04 05:15:42 +00:00
import {
type AgentElevatedAllowFromConfig ,
2026-01-04 14:32:47 +00:00
type ClawdbotConfig ,
2026-01-04 05:15:42 +00:00
loadConfig ,
} from "../config/config.js" ;
2026-01-04 05:47:21 +01:00
import { resolveSessionTranscriptPath } from "../config/sessions.js" ;
2025-12-17 11:29:04 +01:00
import { logVerbose } from "../globals.js" ;
2025-12-26 13:35:44 +01:00
import { clearCommandLane , getQueueSize } from "../process/command-queue.js" ;
2025-12-05 19:03:59 +00:00
import { defaultRuntime } from "../runtime.js" ;
2026-01-06 02:06:06 +01:00
import { resolveCommandAuthorization } from "./command-auth.js" ;
2026-01-05 01:46:07 +01:00
import { hasControlCommand } from "./command-detection.js" ;
2026-01-06 14:17:56 -06:00
import { shouldHandleTextCommands } from "./commands-registry.js" ;
2026-01-04 05:47:21 +01:00
import { getAbortMemory } from "./reply/abort.js" ;
import { runReplyAgent } from "./reply/agent-runner.js" ;
import { resolveBlockStreamingChunking } from "./reply/block-streaming.js" ;
import { applySessionHints } from "./reply/body.js" ;
import { buildCommandContext , handleCommands } from "./reply/commands.js" ;
import {
handleDirectiveOnly ,
2026-01-06 14:17:56 -06:00
type InlineDirectives ,
2026-01-04 05:47:21 +01:00
isDirectiveOnly ,
parseInlineDirectives ,
persistInlineDirectives ,
resolveDefaultModel ,
} from "./reply/directive-handling.js" ;
import {
buildGroupIntro ,
defaultGroupActivation ,
resolveGroupRequireMention ,
} from "./reply/groups.js" ;
2026-01-06 14:17:56 -06:00
import { stripMentions , stripStructuralPrefixes } from "./reply/mentions.js" ;
2025-12-22 20:36:29 +01:00
import {
2026-01-04 05:47:21 +01:00
createModelSelectionState ,
resolveContextTokens ,
} from "./reply/model-selection.js" ;
import { resolveQueueSettings } from "./reply/queue.js" ;
import { initSessionState } from "./reply/session.js" ;
import {
ensureSkillSnapshot ,
prependSystemEvents ,
} from "./reply/session-updates.js" ;
import { createTypingController } from "./reply/typing.js" ;
2026-01-05 06:18:11 +01:00
import type { MsgContext , TemplateContext } from "./templating.js" ;
2025-12-04 17:53:37 +00:00
import {
2026-01-04 05:15:42 +00:00
type ElevatedLevel ,
2026-01-04 06:27:54 +01:00
normalizeThinkLevel ,
2026-01-07 06:16:38 +01:00
type ReasoningLevel ,
2025-12-04 17:53:37 +00:00
type ThinkLevel ,
type VerboseLevel ,
} from "./thinking.js" ;
2026-01-02 01:42:27 +01:00
import { SILENT_REPLY_TOKEN } from "./tokens.js" ;
2025-12-04 18:02:51 +00:00
import { isAudio , transcribeInboundAudio } from "./transcription.js" ;
import type { GetReplyOptions , ReplyPayload } from "./types.js" ;
2025-11-26 02:34:43 +01:00
2026-01-04 05:47:21 +01:00
export {
2026-01-04 05:15:42 +00:00
extractElevatedDirective ,
2026-01-07 06:16:38 +01:00
extractReasoningDirective ,
2026-01-04 05:47:21 +01:00
extractThinkDirective ,
extractVerboseDirective ,
} from "./reply/directives.js" ;
export { extractQueueDirective } from "./reply/queue.js" ;
export { extractReplyToTag } from "./reply/reply-tags.js" ;
2025-11-26 02:34:43 +01:00
export type { GetReplyOptions , ReplyPayload } from "./types.js" ;
2025-11-25 04:58:31 +01:00
2025-12-20 13:04:55 +00:00
const BARE_SESSION_RESET_PROMPT =
2025-12-23 23:45:20 +00:00
"A new session was started via /new or /reset. Say hi briefly (1-2 sentences) and ask what the user wants to do next. Do not mention internal steps, files, tools, or reasoning." ;
2025-12-20 13:04:55 +00:00
2026-01-04 05:15:42 +00:00
function normalizeAllowToken ( value? : string ) {
if ( ! value ) return "" ;
return value . trim ( ) . toLowerCase ( ) ;
}
function slugAllowToken ( value? : string ) {
if ( ! value ) return "" ;
let text = value . trim ( ) . toLowerCase ( ) ;
if ( ! text ) return "" ;
text = text . replace ( /^[@#]+/ , "" ) ;
text = text . replace ( /[\s_]+/g , "-" ) ;
text = text . replace ( /[^a-z0-9-]+/g , "-" ) ;
return text . replace ( /-{2,}/g , "-" ) . replace ( /^-+|-+$/g , "" ) ;
}
function stripSenderPrefix ( value? : string ) {
if ( ! value ) return "" ;
const trimmed = value . trim ( ) ;
return trimmed . replace (
/^(whatsapp|telegram|discord|signal|imessage|webchat|user|group|channel):/i ,
"" ,
) ;
}
function resolveElevatedAllowList (
allowFrom : AgentElevatedAllowFromConfig | undefined ,
2026-01-06 18:25:37 +00:00
provider : string ,
2026-01-04 05:31:00 +00:00
discordFallback? : Array < string | number > ,
2026-01-04 05:15:42 +00:00
) : Array < string | number > | undefined {
2026-01-06 18:25:37 +00:00
switch ( provider ) {
2026-01-04 05:15:42 +00:00
case "whatsapp" :
return allowFrom ? . whatsapp ;
case "telegram" :
return allowFrom ? . telegram ;
2026-01-04 05:31:00 +00:00
case "discord" : {
const hasExplicit = Boolean (
2026-01-04 07:05:04 +01:00
allowFrom && Object . hasOwn ( allowFrom , "discord" ) ,
2026-01-04 05:31:00 +00:00
) ;
if ( hasExplicit ) return allowFrom ? . discord ;
return discordFallback ;
}
2026-01-04 05:15:42 +00:00
case "signal" :
return allowFrom ? . signal ;
case "imessage" :
return allowFrom ? . imessage ;
case "webchat" :
return allowFrom ? . webchat ;
default :
return undefined ;
}
}
function isApprovedElevatedSender ( params : {
2026-01-06 18:25:37 +00:00
provider : string ;
2026-01-04 05:15:42 +00:00
ctx : MsgContext ;
allowFrom? : AgentElevatedAllowFromConfig ;
2026-01-04 05:31:00 +00:00
discordFallback? : Array < string | number > ;
2026-01-04 05:15:42 +00:00
} ) : boolean {
2026-01-04 05:31:00 +00:00
const rawAllow = resolveElevatedAllowList (
params . allowFrom ,
2026-01-06 18:25:37 +00:00
params . provider ,
2026-01-04 05:31:00 +00:00
params . discordFallback ,
) ;
2026-01-04 05:15:42 +00:00
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 ;
const tokens = new Set < string > ( ) ;
const addToken = ( value? : string ) = > {
if ( ! value ) return ;
const trimmed = value . trim ( ) ;
if ( ! trimmed ) return ;
tokens . add ( trimmed ) ;
const normalized = normalizeAllowToken ( trimmed ) ;
if ( normalized ) tokens . add ( normalized ) ;
const slugged = slugAllowToken ( trimmed ) ;
if ( slugged ) tokens . add ( slugged ) ;
} ;
addToken ( params . ctx . SenderName ) ;
2026-01-04 05:47:28 +00:00
addToken ( params . ctx . SenderUsername ) ;
addToken ( params . ctx . SenderTag ) ;
2026-01-04 05:15:42 +00:00
addToken ( params . ctx . SenderE164 ) ;
addToken ( params . ctx . From ) ;
addToken ( stripSenderPrefix ( params . ctx . From ) ) ;
addToken ( params . ctx . To ) ;
addToken ( stripSenderPrefix ( params . ctx . To ) ) ;
for ( const rawEntry of allowTokens ) {
const entry = rawEntry . trim ( ) ;
if ( ! entry ) continue ;
const stripped = stripSenderPrefix ( entry ) ;
if ( tokens . has ( entry ) || tokens . has ( stripped ) ) return true ;
const normalized = normalizeAllowToken ( stripped ) ;
if ( normalized && tokens . has ( normalized ) ) return true ;
const slugged = slugAllowToken ( stripped ) ;
if ( slugged && tokens . has ( slugged ) ) return true ;
}
return false ;
}
2025-11-25 02:16:54 +01:00
export async function getReplyFromConfig (
2025-11-26 00:53:53 +01:00
ctx : MsgContext ,
opts? : GetReplyOptions ,
2026-01-04 14:32:47 +00:00
configOverride? : ClawdbotConfig ,
2025-12-03 00:35:57 +00:00
) : Promise < ReplyPayload | ReplyPayload [ ] | undefined > {
2025-11-26 00:53:53 +01:00
const cfg = configOverride ? ? loadConfig ( ) ;
2025-12-23 23:45:20 +00:00
const agentCfg = cfg . agent ;
2025-12-24 00:22:52 +00:00
const sessionCfg = cfg . session ;
2026-01-04 05:47:21 +01:00
const { defaultProvider , defaultModel , aliasIndex } = resolveDefaultModel ( {
2025-12-26 01:13:13 +01:00
cfg ,
} ) ;
2025-12-23 23:45:20 +00:00
let provider = defaultProvider ;
let model = defaultModel ;
2025-12-26 01:13:13 +01:00
if ( opts ? . isHeartbeat ) {
const heartbeatRaw = agentCfg ? . heartbeat ? . model ? . trim ( ) ? ? "" ;
const heartbeatRef = heartbeatRaw
2025-12-26 23:26:14 +00:00
? resolveModelRefFromString ( {
raw : heartbeatRaw ,
defaultProvider ,
aliasIndex ,
} )
2025-12-26 01:13:13 +01:00
: null ;
if ( heartbeatRef ) {
2025-12-26 23:26:14 +00:00
provider = heartbeatRef . ref . provider ;
model = heartbeatRef . ref . model ;
2025-12-26 01:13:13 +01:00
}
}
2025-12-17 11:29:04 +01:00
2026-01-06 18:25:37 +00:00
const agentId = resolveAgentIdFromSessionKey ( ctx . SessionKey ) ;
const workspaceDirRaw =
resolveAgentWorkspaceDir ( cfg , agentId ) ? ? DEFAULT_AGENT_WORKSPACE_DIR ;
2025-12-17 11:29:04 +01:00
const workspace = await ensureAgentWorkspace ( {
dir : workspaceDirRaw ,
2026-01-07 01:02:51 +08:00
ensureBootstrapFiles : ! cfg . agent ? . skipBootstrap ,
2025-12-17 11:29:04 +01:00
} ) ;
const workspaceDir = workspace . dir ;
2026-01-06 18:25:37 +00:00
const agentDir = resolveAgentDir ( cfg , agentId ) ;
2026-01-06 02:48:44 +00:00
const timeoutMs = resolveAgentTimeoutMs ( { cfg } ) ;
2025-12-23 15:03:05 +00:00
const configuredTypingSeconds =
agentCfg ? . typingIntervalSeconds ? ? sessionCfg ? . typingIntervalSeconds ;
const typingIntervalSeconds =
2025-12-24 00:33:35 +00:00
typeof configuredTypingSeconds === "number" ? configuredTypingSeconds : 6 ;
2026-01-04 05:47:21 +01:00
const typing = createTypingController ( {
onReplyStart : opts?.onReplyStart ,
typingIntervalSeconds ,
silentToken : SILENT_REPLY_TOKEN ,
log : defaultRuntime.log ,
} ) ;
2026-01-06 03:05:11 +00:00
opts ? . onTypingController ? . ( typing ) ;
2025-11-26 00:53:53 +01:00
2026-01-04 05:47:21 +01:00
let transcribedText : string | undefined ;
2025-12-24 00:22:52 +00:00
if ( cfg . routing ? . transcribeAudio && isAudio ( ctx . MediaType ) ) {
2025-11-26 00:53:53 +01:00
const transcribed = await transcribeInboundAudio ( cfg , ctx , defaultRuntime ) ;
if ( transcribed ? . text ) {
transcribedText = transcribed . text ;
ctx . Body = transcribed . text ;
ctx . Transcript = transcribed . text ;
logVerbose ( "Replaced Body with audio transcript for reply flow" ) ;
}
}
2026-01-06 02:06:06 +01:00
const commandAuthorized = ctx . CommandAuthorized ? ? true ;
2026-01-06 14:17:56 -06:00
resolveCommandAuthorization ( {
2026-01-06 02:06:06 +01:00
ctx ,
cfg ,
commandAuthorized ,
} ) ;
const sessionState = await initSessionState ( {
ctx ,
cfg ,
commandAuthorized ,
} ) ;
2026-01-04 05:47:21 +01:00
let {
sessionCtx ,
sessionEntry ,
sessionStore ,
sessionKey ,
2025-12-17 11:29:04 +01:00
sessionId ,
2026-01-04 05:47:21 +01:00
isNewSession ,
2025-12-17 11:29:04 +01:00
systemSent ,
abortedLastRun ,
2026-01-04 05:47:21 +01:00
storePath ,
sessionScope ,
groupResolution ,
isGroup ,
triggerBodyNormalized ,
} = sessionState ;
2026-01-05 01:31:36 +01:00
const rawBody = sessionCtx . BodyStripped ? ? sessionCtx . Body ? ? "" ;
2026-01-06 14:17:56 -06:00
const clearInlineDirectives = ( cleaned : string ) : InlineDirectives = > ( {
cleaned ,
hasThinkDirective : false ,
thinkLevel : undefined ,
rawThinkLevel : undefined ,
hasVerboseDirective : false ,
verboseLevel : undefined ,
rawVerboseLevel : undefined ,
2026-01-07 06:16:38 +01:00
hasReasoningDirective : false ,
reasoningLevel : undefined ,
rawReasoningLevel : undefined ,
2026-01-06 14:17:56 -06:00
hasElevatedDirective : false ,
elevatedLevel : undefined ,
rawElevatedLevel : undefined ,
hasStatusDirective : false ,
hasModelDirective : false ,
rawModelDirective : undefined ,
hasQueueDirective : false ,
queueMode : undefined ,
queueReset : false ,
rawQueueMode : undefined ,
debounceMs : undefined ,
cap : undefined ,
dropPolicy : undefined ,
rawDebounce : undefined ,
rawCap : undefined ,
rawDrop : undefined ,
hasQueueOptions : false ,
} ) ;
let parsedDirectives = parseInlineDirectives ( rawBody ) ;
const hasDirective =
parsedDirectives . hasThinkDirective ||
parsedDirectives . hasVerboseDirective ||
2026-01-07 06:16:38 +01:00
parsedDirectives . hasReasoningDirective ||
2026-01-06 14:17:56 -06:00
parsedDirectives . hasElevatedDirective ||
parsedDirectives . hasStatusDirective ||
parsedDirectives . hasModelDirective ||
parsedDirectives . hasQueueDirective ;
if ( hasDirective ) {
const stripped = stripStructuralPrefixes ( parsedDirectives . cleaned ) ;
const noMentions = isGroup ? stripMentions ( stripped , ctx , cfg ) : stripped ;
if ( noMentions . trim ( ) . length > 0 ) {
parsedDirectives = clearInlineDirectives ( parsedDirectives . cleaned ) ;
}
}
2026-01-05 01:31:36 +01:00
const directives = commandAuthorized
? parsedDirectives
: {
. . . parsedDirectives ,
hasThinkDirective : false ,
hasVerboseDirective : false ,
2026-01-07 06:16:38 +01:00
hasReasoningDirective : false ,
2026-01-05 01:31:36 +01:00
hasStatusDirective : false ,
hasModelDirective : false ,
hasQueueDirective : false ,
queueReset : false ,
} ;
sessionCtx . Body = parsedDirectives . cleaned ;
sessionCtx . BodyStripped = parsedDirectives . cleaned ;
2025-12-23 12:53:30 +00:00
2026-01-06 18:25:37 +00:00
const messageProviderKey =
sessionCtx . Provider ? . trim ( ) . toLowerCase ( ) ? ?
ctx . Provider ? . trim ( ) . toLowerCase ( ) ? ?
2026-01-04 05:15:42 +00:00
"" ;
const elevatedConfig = agentCfg ? . elevated ;
2026-01-04 05:31:00 +00:00
const discordElevatedFallback =
2026-01-06 18:25:37 +00:00
messageProviderKey === "discord" ? cfg.discord?.dm?.allowFrom : undefined ;
2026-01-04 05:15:42 +00:00
const elevatedEnabled = elevatedConfig ? . enabled !== false ;
const elevatedAllowed =
elevatedEnabled &&
Boolean (
2026-01-06 18:25:37 +00:00
messageProviderKey &&
2026-01-04 05:15:42 +00:00
isApprovedElevatedSender ( {
2026-01-06 18:25:37 +00:00
provider : messageProviderKey ,
2026-01-04 05:15:42 +00:00
ctx ,
allowFrom : elevatedConfig?.allowFrom ,
2026-01-04 05:31:00 +00:00
discordFallback : discordElevatedFallback ,
2026-01-04 05:15:42 +00:00
} ) ,
) ;
2026-01-04 06:27:54 +01:00
if (
directives . hasElevatedDirective &&
( ! elevatedEnabled || ! elevatedAllowed )
) {
2026-01-04 05:15:42 +00:00
typing . cleanup ( ) ;
return { text : "elevated is not available right now." } ;
}
2026-01-04 05:47:21 +01:00
const requireMention = resolveGroupRequireMention ( {
cfg ,
ctx : sessionCtx ,
groupResolution ,
} ) ;
const defaultActivation = defaultGroupActivation ( requireMention ) ;
2025-12-03 09:09:34 +00:00
let resolvedThinkLevel =
2026-01-04 05:47:21 +01:00
( directives . thinkLevel as ThinkLevel | undefined ) ? ?
2025-12-03 09:09:34 +00:00
( sessionEntry ? . thinkingLevel as ThinkLevel | undefined ) ? ?
2025-12-17 11:29:04 +01:00
( agentCfg ? . thinkingDefault as ThinkLevel | undefined ) ;
2025-12-03 09:09:34 +00:00
const resolvedVerboseLevel =
2026-01-04 05:47:21 +01:00
( directives . verboseLevel as VerboseLevel | undefined ) ? ?
2025-12-03 09:09:34 +00:00
( sessionEntry ? . verboseLevel as VerboseLevel | undefined ) ? ?
2025-12-17 11:29:04 +01:00
( agentCfg ? . verboseDefault as VerboseLevel | undefined ) ;
2026-01-07 06:16:38 +01:00
const resolvedReasoningLevel : ReasoningLevel =
( directives . reasoningLevel as ReasoningLevel | undefined ) ? ?
( sessionEntry ? . reasoningLevel as ReasoningLevel | undefined ) ? ?
"off" ;
2026-01-04 05:15:42 +00:00
const resolvedElevatedLevel = elevatedAllowed
? ( ( directives . elevatedLevel as ElevatedLevel | undefined ) ? ?
( sessionEntry ? . elevatedLevel as ElevatedLevel | undefined ) ? ?
( agentCfg ? . elevatedDefault as ElevatedLevel | undefined ) ? ?
2026-01-04 05:19:20 +00:00
"on" )
2026-01-04 10:13:28 -06:00
: "off" ;
2026-01-03 00:28:33 +01:00
const resolvedBlockStreaming =
agentCfg ? . blockStreamingDefault === "off" ? "off" : "on" ;
2026-01-04 07:05:04 +01:00
const resolvedBlockStreamingBreak : "text_end" | "message_end" =
2026-01-03 12:35:16 -06:00
agentCfg ? . blockStreamingBreak === "message_end"
? "message_end"
: "text_end" ;
2026-01-03 00:28:33 +01:00
const blockStreamingEnabled = resolvedBlockStreaming === "on" ;
2026-01-03 16:45:53 +01:00
const blockReplyChunking = blockStreamingEnabled
2026-01-06 18:25:37 +00:00
? resolveBlockStreamingChunking ( cfg , sessionCtx . Provider )
2026-01-03 16:45:53 +01:00
: undefined ;
2025-12-23 23:45:20 +00:00
2026-01-04 05:47:21 +01:00
const modelState = await createModelSelectionState ( {
cfg ,
agentCfg ,
sessionEntry ,
sessionStore ,
sessionKey ,
storePath ,
defaultProvider ,
defaultModel ,
provider ,
model ,
hasModelDirective : directives.hasModelDirective ,
} ) ;
provider = modelState . provider ;
model = modelState . model ;
2025-12-23 23:45:20 +00:00
2026-01-04 05:47:21 +01:00
let contextTokens = resolveContextTokens ( {
agentCfg ,
model ,
} ) ;
2025-12-04 02:29:32 +00:00
2025-12-27 01:17:03 +00:00
const initialModelLabel = ` ${ provider } / ${ model } ` ;
const formatModelSwitchEvent = ( label : string , alias? : string ) = >
2025-12-27 04:02:13 +01:00
alias
? ` Model switched to ${ alias } ( ${ label } ). `
: ` Model switched to ${ label } . ` ;
2025-12-27 12:10:44 +00:00
const isModelListAlias =
2026-01-04 05:47:21 +01:00
directives . hasModelDirective &&
2026-01-06 00:56:29 +00:00
[ "status" , "list" ] . includes (
directives . rawModelDirective ? . trim ( ) . toLowerCase ( ) ? ? "" ,
) ;
2025-12-27 12:10:44 +00:00
const effectiveModelDirective = isModelListAlias
? undefined
2026-01-04 05:47:21 +01:00
: directives . rawModelDirective ;
2025-12-23 23:45:20 +00:00
2026-01-04 05:47:21 +01:00
if (
isDirectiveOnly ( {
directives ,
cleanedBody : directives.cleaned ,
ctx ,
cfg ,
isGroup ,
} )
) {
const directiveReply = await handleDirectiveOnly ( {
2026-01-06 00:56:29 +00:00
cfg ,
2026-01-04 05:47:21 +01:00
directives ,
sessionEntry ,
sessionStore ,
sessionKey ,
storePath ,
2026-01-04 05:15:42 +00:00
elevatedEnabled ,
elevatedAllowed ,
2026-01-04 05:47:21 +01:00
defaultProvider ,
defaultModel ,
aliasIndex ,
allowedModelKeys : modelState.allowedModelKeys ,
allowedModelCatalog : modelState.allowedModelCatalog ,
resetModelOverride : modelState.resetModelOverride ,
provider ,
model ,
initialModelLabel ,
formatModelSwitchEvent ,
} ) ;
typing . cleanup ( ) ;
return directiveReply ;
2025-12-03 09:09:34 +00:00
}
2025-12-03 09:04:37 +00:00
2026-01-04 05:47:21 +01:00
const persisted = await persistInlineDirectives ( {
directives ,
effectiveModelDirective ,
2026-01-06 00:56:29 +00:00
cfg ,
2026-01-04 05:47:21 +01:00
sessionEntry ,
sessionStore ,
sessionKey ,
storePath ,
2026-01-04 05:15:42 +00:00
elevatedEnabled ,
elevatedAllowed ,
2026-01-04 05:47:21 +01:00
defaultProvider ,
defaultModel ,
aliasIndex ,
allowedModelKeys : modelState.allowedModelKeys ,
provider ,
model ,
initialModelLabel ,
formatModelSwitchEvent ,
agentCfg ,
} ) ;
provider = persisted . provider ;
model = persisted . model ;
contextTokens = persisted . contextTokens ;
2025-12-26 14:24:53 +01:00
const perMessageQueueMode =
2026-01-04 05:47:21 +01:00
directives . hasQueueDirective && ! directives . queueReset
? directives . queueMode
: undefined ;
2026-01-03 04:26:36 +01:00
const perMessageQueueOptions =
2026-01-04 05:47:21 +01:00
directives . hasQueueDirective && ! directives . queueReset
2026-01-03 04:26:36 +01:00
? {
2026-01-04 05:47:21 +01:00
debounceMs : directives.debounceMs ,
cap : directives.cap ,
dropPolicy : directives.dropPolicy ,
2026-01-03 04:26:36 +01:00
}
: undefined ;
2025-12-05 21:13:17 +00:00
2026-01-04 05:47:21 +01:00
const command = buildCommandContext ( {
ctx ,
cfg ,
sessionKey ,
isGroup ,
triggerBodyNormalized ,
2026-01-05 01:31:36 +01:00
commandAuthorized ,
2026-01-04 05:47:21 +01:00
} ) ;
2026-01-06 14:17:56 -06:00
const allowTextCommands = shouldHandleTextCommands ( {
cfg ,
surface : command.surface ,
commandSource : ctx.CommandSource ,
} ) ;
2026-01-02 17:15:12 +01:00
const isEmptyConfig = Object . keys ( cfg ) . length === 0 ;
2025-12-07 16:53:19 +00:00
if (
2026-01-06 18:25:37 +00:00
command . isWhatsAppProvider &&
2026-01-04 05:47:21 +01:00
isEmptyConfig &&
command . from &&
command . to &&
command . from !== command . to
2025-12-07 16:53:19 +00:00
) {
2026-01-04 05:47:21 +01:00
typing . cleanup ( ) ;
return undefined ;
2025-12-07 16:53:19 +00:00
}
2026-01-04 05:47:21 +01:00
if ( ! sessionEntry && command . abortKey ) {
abortedLastRun = getAbortMemory ( command . abortKey ) ? ? false ;
2025-12-02 20:09:51 +00:00
}
2026-01-04 05:47:21 +01:00
const commandResult = await handleCommands ( {
ctx ,
2026-01-03 23:44:38 +01:00
cfg ,
2026-01-04 05:47:21 +01:00
command ,
2026-01-05 01:31:36 +01:00
directives ,
2026-01-04 05:47:21 +01:00
sessionEntry ,
sessionStore ,
2026-01-03 23:44:38 +01:00
sessionKey ,
2026-01-04 05:47:21 +01:00
storePath ,
sessionScope ,
workspaceDir ,
defaultGroupActivation : ( ) = > defaultActivation ,
resolvedThinkLevel ,
resolvedVerboseLevel : resolvedVerboseLevel ? ? "off" ,
2026-01-07 06:16:38 +01:00
resolvedReasoningLevel ,
2026-01-05 07:07:17 +01:00
resolvedElevatedLevel ,
2026-01-04 05:47:21 +01:00
resolveDefaultThinkingLevel : modelState.resolveDefaultThinkingLevel ,
provider ,
model ,
contextTokens ,
isGroup ,
2026-01-03 23:44:38 +01:00
} ) ;
2026-01-04 05:47:21 +01:00
if ( ! commandResult . shouldContinue ) {
typing . cleanup ( ) ;
return commandResult . reply ;
2026-01-03 23:44:38 +01:00
}
2026-01-05 06:18:11 +01:00
await stageSandboxMedia ( {
ctx ,
sessionCtx ,
cfg ,
sessionKey ,
workspaceDir ,
} ) ;
2025-11-26 00:53:53 +01:00
const isFirstTurnInSession = isNewSession || ! systemSent ;
2025-12-23 14:17:18 +00:00
const isGroupChat = sessionCtx . ChatType === "group" ;
const wasMentioned = ctx . WasMentioned === true ;
2026-01-05 12:03:36 +13:00
const isHeartbeat = opts ? . isHeartbeat === true ;
const shouldEagerType = ( ! isGroupChat || wasMentioned ) && ! isHeartbeat ;
2026-01-04 07:05:04 +01:00
const shouldInjectGroupIntro = Boolean (
2025-12-23 14:17:18 +00:00
isGroupChat &&
2026-01-04 07:05:04 +01:00
( isFirstTurnInSession || sessionEntry ? . groupActivationNeedsSystemIntro ) ,
) ;
2025-12-24 00:33:35 +00:00
const groupIntro = shouldInjectGroupIntro
2026-01-04 05:47:21 +01:00
? buildGroupIntro ( {
sessionCtx ,
sessionEntry ,
defaultActivation ,
silentToken : SILENT_REPLY_TOKEN ,
} )
2025-12-24 00:33:35 +00:00
: "" ;
2025-11-26 00:53:53 +01:00
const baseBody = sessionCtx . BodyStripped ? ? sessionCtx . Body ? ? "" ;
2025-12-10 15:55:20 +00:00
const rawBodyTrimmed = ( ctx . Body ? ? "" ) . trim ( ) ;
2025-12-20 13:04:55 +00:00
const baseBodyTrimmedRaw = baseBody . trim ( ) ;
2026-01-06 02:06:06 +01:00
if (
2026-01-06 14:17:56 -06:00
allowTextCommands &&
! commandAuthorized &&
! baseBodyTrimmedRaw &&
hasControlCommand ( rawBody )
2026-01-06 02:06:06 +01:00
) {
typing . cleanup ( ) ;
return undefined ;
}
2025-12-10 15:55:20 +00:00
const isBareSessionReset =
2025-12-20 13:04:55 +00:00
isNewSession &&
baseBodyTrimmedRaw . length === 0 &&
rawBodyTrimmed . length > 0 ;
2025-12-20 13:31:28 +00:00
const baseBodyFinal = isBareSessionReset
? BARE_SESSION_RESET_PROMPT
: baseBody ;
2025-12-20 13:04:55 +00:00
const baseBodyTrimmed = baseBodyFinal . trim ( ) ;
2025-12-10 15:55:20 +00:00
if ( ! baseBodyTrimmed ) {
2026-01-04 05:47:21 +01:00
await typing . onReplyStart ( ) ;
2025-12-10 13:51:06 +00:00
logVerbose ( "Inbound body empty after normalization; skipping agent run" ) ;
2026-01-04 05:47:21 +01:00
typing . cleanup ( ) ;
2025-12-10 13:51:06 +00:00
return {
2025-12-10 15:55:20 +00:00
text : "I didn't receive any text in your message. Please resend or add a caption." ,
2025-12-10 13:51:06 +00:00
} ;
}
2026-01-04 05:47:21 +01:00
let prefixedBodyBase = await applySessionHints ( {
baseBody : baseBodyFinal ,
abortedLastRun ,
sessionEntry ,
sessionStore ,
sessionKey ,
storePath ,
abortKey : command.abortKey ,
messageId : sessionCtx.MessageSid ,
} ) ;
2025-12-09 02:25:37 +01:00
const isGroupSession =
2026-01-02 10:14:58 +01:00
sessionEntry ? . chatType === "group" || sessionEntry ? . chatType === "room" ;
2025-12-09 02:25:37 +01:00
const isMainSession =
! isGroupSession && sessionKey === ( sessionCfg ? . mainKey ? ? "main" ) ;
2026-01-04 05:47:21 +01:00
prefixedBodyBase = await prependSystemEvents ( {
cfg ,
2026-01-04 22:11:04 +01:00
sessionKey ,
2026-01-04 05:47:21 +01:00
isMainSession ,
isNewSession ,
prefixedBodyBase ,
} ) ;
const skillResult = await ensureSkillSnapshot ( {
sessionEntry ,
sessionStore ,
sessionKey ,
storePath ,
sessionId ,
isFirstTurnInSession ,
workspaceDir ,
cfg ,
} ) ;
sessionEntry = skillResult . sessionEntry ? ? sessionEntry ;
systemSent = skillResult . systemSent ;
const skillsSnapshot = skillResult . skillsSnapshot ;
2025-12-17 11:29:04 +01:00
const prefixedBody = transcribedText
? [ prefixedBodyBase , ` Transcript: \ n ${ transcribedText } ` ]
. filter ( Boolean )
. join ( "\n\n" )
: prefixedBodyBase ;
2025-11-26 00:53:53 +01:00
const mediaNote = ctx . MediaPath ? . length
? ` [media attached: ${ ctx . MediaPath } ${ ctx . MediaType ? ` ( ${ ctx . MediaType } ) ` : "" } ${ ctx . MediaUrl ? ` | ${ ctx . MediaUrl } ` : "" } ] `
: undefined ;
2025-12-17 11:29:04 +01:00
const mediaReplyHint = mediaNote
? "To send an image back, add a line like: MEDIA:https://example.com/image.jpg (no spaces). Keep caption in the text body."
: undefined ;
2025-12-03 08:45:23 +00:00
let commandBody = mediaNote
2025-11-26 00:53:53 +01:00
? [ mediaNote , mediaReplyHint , prefixedBody ? ? "" ]
. filter ( Boolean )
. join ( "\n" )
. trim ( )
: prefixedBody ;
2025-12-03 08:45:23 +00:00
if ( ! resolvedThinkLevel && commandBody ) {
const parts = commandBody . split ( /\s+/ ) ;
const maybeLevel = normalizeThinkLevel ( parts [ 0 ] ) ;
if ( maybeLevel ) {
resolvedThinkLevel = maybeLevel ;
commandBody = parts . slice ( 1 ) . join ( " " ) . trim ( ) ;
}
}
2026-01-03 12:18:50 +00:00
if ( ! resolvedThinkLevel ) {
2026-01-04 05:47:21 +01:00
resolvedThinkLevel = await modelState . resolveDefaultThinkingLevel ( ) ;
2026-01-03 12:18:50 +00:00
}
2025-12-17 11:29:04 +01:00
const sessionIdFinal = sessionId ? ? crypto . randomUUID ( ) ;
const sessionFile = resolveSessionTranscriptPath ( sessionIdFinal ) ;
2025-12-20 16:10:46 +01:00
const queueBodyBase = transcribedText
? [ baseBodyFinal , ` Transcript: \ n ${ transcribedText } ` ]
. filter ( Boolean )
. join ( "\n\n" )
: baseBodyFinal ;
const queuedBody = mediaNote
2025-12-20 17:50:45 +01:00
? [ mediaNote , mediaReplyHint , queueBodyBase ]
. filter ( Boolean )
. join ( "\n" )
. trim ( )
2025-12-20 16:10:46 +01:00
: queueBodyBase ;
2026-01-03 04:26:36 +01:00
const resolvedQueue = resolveQueueSettings ( {
2025-12-26 13:35:44 +01:00
cfg ,
2026-01-06 18:25:37 +00:00
provider : sessionCtx.Provider ,
2025-12-26 13:35:44 +01:00
sessionEntry ,
inlineMode : perMessageQueueMode ,
2026-01-03 04:26:36 +01:00
inlineOptions : perMessageQueueOptions ,
2025-12-26 13:35:44 +01:00
} ) ;
const sessionLaneKey = resolveEmbeddedSessionLane (
sessionKey ? ? sessionIdFinal ,
) ;
const laneSize = getQueueSize ( sessionLaneKey ) ;
2026-01-03 04:26:36 +01:00
if ( resolvedQueue . mode === "interrupt" && laneSize > 0 ) {
2025-12-26 13:35:44 +01:00
const cleared = clearCommandLane ( sessionLaneKey ) ;
const aborted = abortEmbeddedPiRun ( sessionIdFinal ) ;
logVerbose (
` Interrupting ${ sessionLaneKey } (cleared ${ cleared } , aborted= ${ aborted } ) ` ,
) ;
}
2026-01-03 04:26:36 +01:00
const queueKey = sessionKey ? ? sessionIdFinal ;
const isActive = isEmbeddedPiRunActive ( sessionIdFinal ) ;
const isStreaming = isEmbeddedPiRunStreaming ( sessionIdFinal ) ;
const shouldSteer =
resolvedQueue . mode === "steer" || resolvedQueue . mode === "steer-backlog" ;
const shouldFollowup =
resolvedQueue . mode === "followup" ||
resolvedQueue . mode === "collect" ||
resolvedQueue . mode === "steer-backlog" ;
2026-01-06 00:56:29 +00:00
const authProfileId = sessionEntry ? . authProfileOverride ;
2026-01-04 05:47:21 +01:00
const followupRun = {
2026-01-03 04:26:36 +01:00
prompt : queuedBody ,
summaryLine : baseBodyTrimmedRaw ,
enqueuedAt : Date.now ( ) ,
2026-01-06 10:58:45 -08:00
// Originating channel for reply routing.
originatingChannel : ctx.OriginatingChannel ,
originatingTo : ctx.OriginatingTo ,
2026-01-07 05:02:34 +00:00
originatingAccountId : ctx.AccountId ,
originatingThreadId : ctx.MessageThreadId ,
2026-01-03 04:26:36 +01:00
run : {
2026-01-06 18:25:37 +00:00
agentId ,
agentDir ,
2026-01-03 04:26:36 +01:00
sessionId : sessionIdFinal ,
sessionKey ,
2026-01-06 18:25:37 +00:00
messageProvider : sessionCtx.Provider?.trim ( ) . toLowerCase ( ) || undefined ,
2026-01-03 04:26:36 +01:00
sessionFile ,
workspaceDir ,
config : cfg ,
skillsSnapshot ,
provider ,
model ,
2026-01-06 00:56:29 +00:00
authProfileId ,
2026-01-03 04:26:36 +01:00
thinkLevel : resolvedThinkLevel ,
verboseLevel : resolvedVerboseLevel ,
2026-01-07 06:16:38 +01:00
reasoningLevel : resolvedReasoningLevel ,
2026-01-04 05:15:42 +00:00
elevatedLevel : resolvedElevatedLevel ,
bashElevated : {
enabled : elevatedEnabled ,
allowed : elevatedAllowed ,
defaultLevel : resolvedElevatedLevel ? ? "off" ,
} ,
2026-01-03 04:26:36 +01:00
timeoutMs ,
blockReplyBreak : resolvedBlockStreamingBreak ,
2026-01-04 05:47:21 +01:00
ownerNumbers :
command . ownerList . length > 0 ? command.ownerList : undefined ,
2026-01-03 04:26:36 +01:00
extraSystemPrompt : groupIntro || undefined ,
2026-01-04 07:05:04 +01:00
. . . ( provider === "ollama" ? { enforceFinalTag : true } : { } ) ,
2026-01-03 04:26:36 +01:00
} ,
} ;
2026-01-04 05:47:21 +01:00
if ( shouldEagerType ) {
await typing . startTypingLoop ( ) ;
}
return runReplyAgent ( {
commandBody ,
followupRun ,
queueKey ,
resolvedQueue ,
shouldSteer ,
shouldFollowup ,
isActive ,
isStreaming ,
opts ,
typing ,
sessionEntry ,
sessionStore ,
sessionKey ,
storePath ,
defaultModel ,
agentCfgContextTokens : agentCfg?.contextTokens ,
resolvedVerboseLevel : resolvedVerboseLevel ? ? "off" ,
isNewSession ,
blockStreamingEnabled ,
blockReplyChunking ,
resolvedBlockStreamingBreak ,
sessionCtx ,
shouldInjectGroupIntro ,
} ) ;
2025-11-25 02:16:54 +01:00
}
2026-01-05 06:18:11 +01:00
async function stageSandboxMedia ( params : {
ctx : MsgContext ;
sessionCtx : TemplateContext ;
cfg : ClawdbotConfig ;
sessionKey? : string ;
workspaceDir : string ;
} ) {
const { ctx , sessionCtx , cfg , sessionKey , workspaceDir } = params ;
const rawPath = ctx . MediaPath ? . trim ( ) ;
if ( ! rawPath || ! sessionKey ) return ;
const sandbox = await ensureSandboxWorkspaceForSession ( {
config : cfg ,
sessionKey ,
workspaceDir ,
} ) ;
if ( ! sandbox ) return ;
let source = rawPath ;
if ( source . startsWith ( "file://" ) ) {
try {
source = fileURLToPath ( source ) ;
} catch {
return ;
}
}
if ( ! path . isAbsolute ( source ) ) return ;
const originalMediaPath = ctx . MediaPath ;
const originalMediaUrl = ctx . MediaUrl ;
try {
const fileName = path . basename ( source ) ;
if ( ! fileName ) return ;
const destDir = path . join ( sandbox . workspaceDir , "media" , "inbound" ) ;
await fs . mkdir ( destDir , { recursive : true } ) ;
const dest = path . join ( destDir , fileName ) ;
await fs . copyFile ( source , dest ) ;
const relative = path . posix . join ( "media" , "inbound" , fileName ) ;
ctx . MediaPath = relative ;
sessionCtx . MediaPath = relative ;
if ( originalMediaUrl ) {
let normalizedUrl = originalMediaUrl ;
if ( normalizedUrl . startsWith ( "file://" ) ) {
try {
normalizedUrl = fileURLToPath ( normalizedUrl ) ;
} catch {
normalizedUrl = originalMediaUrl ;
}
}
if ( normalizedUrl === originalMediaPath || normalizedUrl === source ) {
ctx . MediaUrl = relative ;
sessionCtx . MediaUrl = relative ;
}
}
} catch ( err ) {
logVerbose ( ` Failed to stage inbound media for sandbox: ${ String ( err ) } ` ) ;
}
}