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 {
2026-01-09 20:42:16 +00:00
resolveAgentConfig ,
2026-01-06 18:25:37 +00:00
resolveAgentDir ,
resolveAgentWorkspaceDir ,
2026-01-10 01:57:33 +01:00
resolveSessionAgentId ,
2026-01-06 18:25:37 +00:00
} 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-10 20:28:34 +01:00
import {
ensureSandboxWorkspaceForSession ,
resolveSandboxRuntimeStatus ,
} 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-13 06:47:35 +00:00
import {
resolveSessionFilePath ,
saveSessionStore ,
} 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" ;
2026-01-11 11:45:25 +00:00
import { getProviderDock } from "../providers/dock.js" ;
import {
CHAT_PROVIDER_ORDER ,
normalizeProviderId ,
} from "../providers/registry.js" ;
2026-01-09 22:30:10 +01:00
import { normalizeMainKey } from "../routing/session-key.js" ;
2025-12-05 19:03:59 +00:00
import { defaultRuntime } from "../runtime.js" ;
2026-01-11 11:45:25 +00:00
import { INTERNAL_MESSAGE_PROVIDER } from "../utils/message-provider.js" ;
2026-01-13 09:47:04 +13:00
import { isReasoningTagProvider } from "../utils/provider-utils.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-07 13:41:40 +00:00
import {
listChatCommands ,
shouldHandleTextCommands ,
} from "./commands-registry.js" ;
2026-01-08 04:44:11 +00:00
import { buildInboundMediaNote } from "./media-note.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" ;
2026-01-09 03:46:00 +01:00
import {
buildCommandContext ,
buildStatusReply ,
handleCommands ,
} from "./reply/commands.js" ;
2026-01-04 05:47:21 +01:00
import {
2026-01-12 22:33:59 +00:00
applyInlineDirectivesFastLane ,
2026-01-04 05:47:21 +01:00
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-10 17:32:19 +01:00
import {
CURRENT_MESSAGE_MARKER ,
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-07 21:58:54 +00:00
import {
2026-01-07 22:18:11 +00:00
createTypingSignaler ,
2026-01-07 21:58:54 +00:00
resolveTypingMode ,
} from "./reply/typing-mode.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-07 17:17:38 -08:00
formatXHighModelHint ,
2026-01-04 06:27:54 +01:00
normalizeThinkLevel ,
2026-01-07 06:16:38 +01:00
type ReasoningLevel ,
2026-01-07 17:17:38 -08:00
supportsXHighThinking ,
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" ;
2026-01-11 01:51:07 +01:00
import {
hasAudioTranscriptionConfig ,
isAudio ,
transcribeInboundAudio ,
} from "./transcription.js" ;
2025-12-04 18:02:51 +00:00
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 , "" ) ;
}
2026-01-11 11:45:25 +00:00
const SENDER_PREFIXES = [
. . . CHAT_PROVIDER_ORDER ,
INTERNAL_MESSAGE_PROVIDER ,
"user" ,
"group" ,
"channel" ,
] ;
const SENDER_PREFIX_RE = new RegExp ( ` ^( ${ SENDER_PREFIXES . join ( "|" ) } ): ` , "i" ) ;
2026-01-04 05:15:42 +00:00
function stripSenderPrefix ( value? : string ) {
if ( ! value ) return "" ;
const trimmed = value . trim ( ) ;
2026-01-11 11:45:25 +00:00
return trimmed . replace ( SENDER_PREFIX_RE , "" ) ;
2026-01-04 05:15:42 +00:00
}
2026-01-12 06:10:17 +00:00
const INLINE_SIMPLE_COMMAND_ALIASES = new Map < string , string > ( [
[ "/help" , "/help" ] ,
[ "/commands" , "/commands" ] ,
[ "/whoami" , "/whoami" ] ,
[ "/id" , "/whoami" ] ,
] ) ;
const INLINE_SIMPLE_COMMAND_RE =
/(?:^|\s)\/(help|commands|whoami|id)(?=$|\s|:)/i ;
2026-01-12 07:11:59 +00:00
const INLINE_STATUS_RE = /(?:^|\s)\/(?:status|usage)(?=$|\s|:)(?:\s*:\s*)?/gi ;
2026-01-12 06:10:17 +00:00
function extractInlineSimpleCommand ( body? : string ) : {
command : string ;
cleaned : string ;
} | null {
if ( ! body ) return null ;
const match = body . match ( INLINE_SIMPLE_COMMAND_RE ) ;
if ( ! match || match . index === undefined ) return null ;
const alias = ` / ${ match [ 1 ] . toLowerCase ( ) } ` ;
const command = INLINE_SIMPLE_COMMAND_ALIASES . get ( alias ) ;
if ( ! command ) return null ;
const cleaned = body . replace ( match [ 0 ] , " " ) . replace ( /\s+/g , " " ) . trim ( ) ;
return { command , cleaned } ;
}
2026-01-12 07:11:59 +00:00
function stripInlineStatus ( body : string ) : {
cleaned : string ;
didStrip : boolean ;
} {
const trimmed = body . trim ( ) ;
if ( ! trimmed ) return { cleaned : "" , didStrip : false } ;
const cleaned = trimmed
. replace ( INLINE_STATUS_RE , " " )
. replace ( /\s+/g , " " )
. trim ( ) ;
return { cleaned , didStrip : cleaned !== trimmed } ;
}
2026-01-04 05:15:42 +00:00
function resolveElevatedAllowList (
allowFrom : AgentElevatedAllowFromConfig | undefined ,
2026-01-06 18:25:37 +00:00
provider : string ,
2026-01-11 11:45:25 +00:00
fallbackAllowFrom? : Array < string | number > ,
2026-01-04 05:15:42 +00:00
) : Array < string | number > | undefined {
2026-01-11 11:45:25 +00:00
if ( ! allowFrom ) return fallbackAllowFrom ;
const value = allowFrom [ provider ] ;
return Array . isArray ( value ) ? value : fallbackAllowFrom ;
2026-01-04 05:15:42 +00:00
}
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-11 11:45:25 +00:00
fallbackAllowFrom? : 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-11 11:45:25 +00:00
params . fallbackAllowFrom ,
2026-01-04 05:31:00 +00:00
) ;
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 ;
}
2026-01-09 20:42:16 +00:00
function resolveElevatedPermissions ( params : {
cfg : ClawdbotConfig ;
agentId : string ;
ctx : MsgContext ;
provider : string ;
2026-01-10 19:47:17 +00:00
} ) : {
enabled : boolean ;
allowed : boolean ;
failures : Array < { gate : string ; key : string } > ;
} {
2026-01-09 20:42:16 +00:00
const globalConfig = params . cfg . tools ? . elevated ;
const agentConfig = resolveAgentConfig ( params . cfg , params . agentId ) ? . tools
? . elevated ;
const globalEnabled = globalConfig ? . enabled !== false ;
const agentEnabled = agentConfig ? . enabled !== false ;
const enabled = globalEnabled && agentEnabled ;
2026-01-10 20:28:34 +01:00
const failures : Array < { gate : string ; key : string } > = [ ] ;
2026-01-10 19:47:17 +00:00
if ( ! globalEnabled )
failures . push ( { gate : "enabled" , key : "tools.elevated.enabled" } ) ;
2026-01-10 20:28:34 +01:00
if ( ! agentEnabled )
2026-01-10 19:47:17 +00:00
failures . push ( {
gate : "enabled" ,
key : "agents.list[].tools.elevated.enabled" ,
} ) ;
2026-01-10 20:28:34 +01:00
if ( ! enabled ) return { enabled , allowed : false , failures } ;
if ( ! params . provider ) {
failures . push ( { gate : "provider" , key : "ctx.Provider" } ) ;
return { enabled , allowed : false , failures } ;
}
2026-01-09 20:42:16 +00:00
2026-01-11 11:45:25 +00:00
const normalizedProvider = normalizeProviderId ( params . provider ) ;
const dockFallbackAllowFrom = normalizedProvider
? getProviderDock ( normalizedProvider ) ? . elevated ? . allowFromFallback ? . ( {
cfg : params.cfg ,
accountId : params.ctx.AccountId ,
} )
: undefined ;
const fallbackAllowFrom = dockFallbackAllowFrom ;
2026-01-09 20:42:16 +00:00
const globalAllowed = isApprovedElevatedSender ( {
provider : params.provider ,
ctx : params.ctx ,
allowFrom : globalConfig?.allowFrom ,
2026-01-11 11:45:25 +00:00
fallbackAllowFrom ,
2026-01-09 20:42:16 +00:00
} ) ;
2026-01-10 20:28:34 +01:00
if ( ! globalAllowed ) {
failures . push ( {
gate : "allowFrom" ,
2026-01-11 11:45:25 +00:00
key : ` tools.elevated.allowFrom. ${ params . provider } ` ,
2026-01-10 20:28:34 +01:00
} ) ;
return { enabled , allowed : false , failures } ;
}
2026-01-09 20:42:16 +00:00
const agentAllowed = agentConfig ? . allowFrom
? isApprovedElevatedSender ( {
provider : params.provider ,
ctx : params.ctx ,
allowFrom : agentConfig.allowFrom ,
2026-01-11 11:45:25 +00:00
fallbackAllowFrom ,
2026-01-09 20:42:16 +00:00
} )
: true ;
2026-01-10 20:28:34 +01:00
if ( ! agentAllowed ) {
failures . push ( {
gate : "allowFrom" ,
key : ` agents.list[].tools.elevated.allowFrom. ${ params . provider } ` ,
} ) ;
}
return { enabled , allowed : globalAllowed && agentAllowed , failures } ;
}
function formatElevatedUnavailableMessage ( params : {
runtimeSandboxed : boolean ;
failures : Array < { gate : string ; key : string } > ;
sessionKey? : string ;
} ) : string {
const lines : string [ ] = [ ] ;
lines . push (
` elevated is not available right now (runtime= ${ params . runtimeSandboxed ? "sandboxed" : "direct" } ). ` ,
) ;
if ( params . failures . length > 0 ) {
lines . push (
` Failing gates: ${ params . failures
. map ( ( f ) = > ` ${ f . gate } ( ${ f . key } ) ` )
. join ( ", " ) } ` ,
) ;
} else {
lines . push (
"Failing gates: enabled (tools.elevated.enabled / agents.list[].tools.elevated.enabled), allowFrom (tools.elevated.allowFrom.<provider>)." ,
) ;
}
lines . push ( "Fix-it keys:" ) ;
lines . push ( "- tools.elevated.enabled" ) ;
lines . push ( "- tools.elevated.allowFrom.<provider>" ) ;
lines . push ( "- agents.list[].tools.elevated.enabled" ) ;
lines . push ( "- agents.list[].tools.elevated.allowFrom.<provider>" ) ;
if ( params . sessionKey ) {
lines . push ( ` See: clawdbot sandbox explain --session ${ params . sessionKey } ` ) ;
}
return lines . join ( "\n" ) ;
2026-01-09 20:42:16 +00:00
}
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 ( ) ;
2026-01-10 01:57:33 +01:00
const agentId = resolveSessionAgentId ( {
sessionKey : ctx.SessionKey ,
config : cfg ,
} ) ;
2026-01-09 12:44:23 +00:00
const agentCfg = cfg . agents ? . defaults ;
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 ,
2026-01-07 09:58:54 +01:00
agentId ,
2025-12-26 01:13:13 +01:00
} ) ;
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 workspaceDirRaw =
resolveAgentWorkspaceDir ( cfg , agentId ) ? ? DEFAULT_AGENT_WORKSPACE_DIR ;
2025-12-17 11:29:04 +01:00
const workspace = await ensureAgentWorkspace ( {
dir : workspaceDirRaw ,
2026-01-09 12:44:23 +00:00
ensureBootstrapFiles : ! agentCfg ? . 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 ;
2026-01-11 01:51:07 +01:00
if ( hasAudioTranscriptionConfig ( cfg ) && 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-10 18:53:33 +01:00
// Prefer CommandBody/RawBody (clean message without structural context) for directive parsing.
2026-01-10 17:32:19 +01:00
// Keep `Body`/`BodyStripped` as the best-available prompt text (may include context).
2026-01-10 18:53:33 +01:00
const commandSource =
sessionCtx . CommandBody ? ?
sessionCtx . RawBody ? ?
sessionCtx . BodyStripped ? ?
sessionCtx . Body ? ?
"" ;
2026-01-12 06:29:26 +00:00
const command = buildCommandContext ( {
ctx ,
cfg ,
agentId ,
sessionKey ,
isGroup ,
triggerBodyNormalized ,
commandAuthorized ,
} ) ;
const allowTextCommands = shouldHandleTextCommands ( {
cfg ,
surface : command.surface ,
commandSource : ctx.CommandSource ,
} ) ;
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 ,
} ) ;
2026-01-07 13:41:40 +00:00
const reservedCommands = new Set (
listChatCommands ( ) . flatMap ( ( cmd ) = >
cmd . textAliases . map ( ( a ) = > a . replace ( /^\// , "" ) . toLowerCase ( ) ) ,
) ,
) ;
2026-01-09 12:44:23 +00:00
const configuredAliases = Object . values ( cfg . agents ? . defaults ? . models ? ? { } )
2026-01-07 19:58:23 +00:00
. map ( ( entry ) = > entry . alias ? . trim ( ) )
2026-01-07 13:41:40 +00:00
. filter ( ( alias ) : alias is string = > Boolean ( alias ) )
. filter ( ( alias ) = > ! reservedCommands . has ( alias . toLowerCase ( ) ) ) ;
2026-01-12 08:36:31 +00:00
const allowStatusDirective = allowTextCommands && command . isAuthorizedSender ;
2026-01-10 18:53:33 +01:00
let parsedDirectives = parseInlineDirectives ( commandSource , {
2026-01-07 13:33:41 +00:00
modelAliases : configuredAliases ,
2026-01-12 08:36:31 +00:00
allowStatusDirective ,
2026-01-07 13:33:41 +00:00
} ) ;
2026-01-12 11:22:56 +00:00
const hasInlineStatus =
parsedDirectives . hasStatusDirective &&
parsedDirectives . cleaned . trim ( ) . length > 0 ;
if ( hasInlineStatus ) {
parsedDirectives = {
. . . parsedDirectives ,
hasStatusDirective : false ,
} ;
}
2026-01-09 03:18:41 +01:00
if (
isGroup &&
ctx . WasMentioned !== true &&
parsedDirectives . hasElevatedDirective
) {
2026-01-09 03:09:50 +01:00
if ( parsedDirectives . elevatedLevel !== "off" ) {
parsedDirectives = {
. . . parsedDirectives ,
hasElevatedDirective : false ,
elevatedLevel : undefined ,
rawElevatedLevel : undefined ,
} ;
}
}
2026-01-12 06:34:30 +00:00
const hasInlineDirective =
2026-01-06 14:17:56 -06:00
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 . hasModelDirective ||
parsedDirectives . hasQueueDirective ;
2026-01-12 06:34:30 +00:00
if ( hasInlineDirective ) {
2026-01-06 14:17:56 -06:00
const stripped = stripStructuralPrefixes ( parsedDirectives . cleaned ) ;
2026-01-08 22:57:08 +01:00
const noMentions = isGroup
? stripMentions ( stripped , ctx , cfg , agentId )
: stripped ;
2026-01-06 14:17:56 -06:00
if ( noMentions . trim ( ) . length > 0 ) {
2026-01-09 03:09:50 +01:00
const directiveOnlyCheck = parseInlineDirectives ( noMentions , {
modelAliases : configuredAliases ,
} ) ;
if ( directiveOnlyCheck . cleaned . trim ( ) . length > 0 ) {
2026-01-12 06:29:26 +00:00
const allowInlineStatus =
parsedDirectives . hasStatusDirective &&
allowTextCommands &&
command . isAuthorizedSender ;
parsedDirectives = allowInlineStatus
? {
. . . clearInlineDirectives ( parsedDirectives . cleaned ) ,
hasStatusDirective : true ,
}
: clearInlineDirectives ( parsedDirectives . cleaned ) ;
2026-01-09 03:09:50 +01:00
}
2026-01-06 14:17:56 -06:00
}
}
2026-01-12 06:10:17 +00:00
let directives = commandAuthorized
2026-01-05 01:31:36 +01:00
? 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 ,
} ;
2026-01-10 17:32:19 +01:00
const existingBody = sessionCtx . BodyStripped ? ? sessionCtx . Body ? ? "" ;
2026-01-12 06:10:17 +00:00
let cleanedBody = ( ( ) = > {
2026-01-10 17:32:19 +01:00
if ( ! existingBody ) return parsedDirectives . cleaned ;
2026-01-10 18:53:33 +01:00
if ( ! sessionCtx . CommandBody && ! sessionCtx . RawBody ) {
2026-01-10 17:32:19 +01:00
return parseInlineDirectives ( existingBody , {
modelAliases : configuredAliases ,
2026-01-12 08:36:31 +00:00
allowStatusDirective ,
2026-01-10 17:32:19 +01:00
} ) . cleaned ;
}
const markerIndex = existingBody . indexOf ( CURRENT_MESSAGE_MARKER ) ;
if ( markerIndex < 0 ) {
return parseInlineDirectives ( existingBody , {
modelAliases : configuredAliases ,
2026-01-12 08:36:31 +00:00
allowStatusDirective ,
2026-01-10 17:32:19 +01:00
} ) . cleaned ;
}
const head = existingBody . slice (
0 ,
markerIndex + CURRENT_MESSAGE_MARKER . length ,
) ;
const tail = existingBody . slice (
markerIndex + CURRENT_MESSAGE_MARKER . length ,
) ;
const cleanedTail = parseInlineDirectives ( tail , {
modelAliases : configuredAliases ,
2026-01-12 08:36:31 +00:00
allowStatusDirective ,
2026-01-10 17:32:19 +01:00
} ) . cleaned ;
return ` ${ head } ${ cleanedTail } ` ;
} ) ( ) ;
2026-01-13 01:20:52 +00:00
if ( allowStatusDirective ) {
cleanedBody = stripInlineStatus ( cleanedBody ) . cleaned ;
}
2026-01-12 07:11:59 +00:00
2026-01-10 17:32:19 +01:00
sessionCtx . Body = cleanedBody ;
sessionCtx . BodyStripped = cleanedBody ;
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
"" ;
2026-01-10 20:28:34 +01:00
const elevated = resolveElevatedPermissions ( {
cfg ,
agentId ,
ctx ,
provider : messageProviderKey ,
} ) ;
const elevatedEnabled = elevated . enabled ;
const elevatedAllowed = elevated . allowed ;
const elevatedFailures = elevated . failures ;
2026-01-04 06:27:54 +01:00
if (
directives . hasElevatedDirective &&
( ! elevatedEnabled || ! elevatedAllowed )
) {
2026-01-04 05:15:42 +00:00
typing . cleanup ( ) ;
2026-01-10 20:28:34 +01:00
const runtimeSandboxed = resolveSandboxRuntimeStatus ( {
cfg ,
sessionKey : ctx.SessionKey ,
} ) . sandboxed ;
return {
text : formatElevatedUnavailableMessage ( {
runtimeSandboxed ,
failures : elevatedFailures ,
sessionKey : ctx.SessionKey ,
} ) ,
} ;
2026-01-04 05:15:42 +00:00
}
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 =
2026-01-09 11:57:43 +13:00
opts ? . disableBlockStreaming === true
? "off"
: opts ? . disableBlockStreaming === false
? "on"
2026-01-09 22:40:58 +01:00
: agentCfg ? . blockStreamingDefault === "on"
? "on"
: "off" ;
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-11 11:45:25 +00:00
const blockStreamingEnabled =
resolvedBlockStreaming === "on" && opts ? . disableBlockStreaming !== true ;
2026-01-03 16:45:53 +01:00
const blockReplyChunking = blockStreamingEnabled
2026-01-09 18:19:55 +00:00
? resolveBlockStreamingChunking (
cfg ,
sessionCtx . Provider ,
sessionCtx . AccountId ,
)
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-12 11:22:56 +00:00
const inlineStatusRequested =
hasInlineStatus && allowTextCommands && command . isAuthorizedSender ;
2026-01-12 21:44:09 +00:00
// Inline control directives should apply immediately, even when mixed with text.
let directiveAck : ReplyPayload | undefined ;
2026-01-12 06:10:17 +00:00
if ( ! command . isAuthorizedSender ) {
directives = {
. . . directives ,
hasThinkDirective : false ,
hasVerboseDirective : false ,
hasReasoningDirective : false ,
hasElevatedDirective : false ,
hasStatusDirective : false ,
hasModelDirective : false ,
hasQueueDirective : false ,
queueReset : false ,
} ;
}
2026-01-09 03:46:00 +01:00
2026-01-04 05:47:21 +01:00
if (
isDirectiveOnly ( {
directives ,
cleanedBody : directives.cleaned ,
ctx ,
cfg ,
2026-01-08 22:57:08 +01:00
agentId ,
2026-01-04 05:47:21 +01:00
isGroup ,
} )
) {
2026-01-12 06:10:17 +00:00
if ( ! command . isAuthorizedSender ) {
typing . cleanup ( ) ;
return undefined ;
}
2026-01-12 03:00:27 +00:00
const resolvedDefaultThinkLevel =
2026-01-07 12:21:20 +01:00
( sessionEntry ? . thinkingLevel as ThinkLevel | undefined ) ? ?
2026-01-12 02:21:10 +00:00
( agentCfg ? . thinkingDefault as ThinkLevel | undefined ) ? ?
( await modelState . resolveDefaultThinkingLevel ( ) ) ;
2026-01-12 03:00:27 +00:00
const currentThinkLevel = resolvedDefaultThinkLevel ;
2026-01-08 03:22:14 +01:00
const currentVerboseLevel =
( sessionEntry ? . verboseLevel as VerboseLevel | undefined ) ? ?
( agentCfg ? . verboseDefault as VerboseLevel | undefined ) ;
const currentReasoningLevel =
( sessionEntry ? . reasoningLevel as ReasoningLevel | undefined ) ? ? "off" ;
const currentElevatedLevel =
( sessionEntry ? . elevatedLevel as ElevatedLevel | undefined ) ? ?
( agentCfg ? . elevatedDefault as ElevatedLevel | undefined ) ;
2026-01-04 05:47:21 +01:00
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-10 20:28:34 +01:00
elevatedFailures ,
messageProviderKey ,
2026-01-04 05:47:21 +01:00
defaultProvider ,
defaultModel ,
aliasIndex ,
allowedModelKeys : modelState.allowedModelKeys ,
allowedModelCatalog : modelState.allowedModelCatalog ,
resetModelOverride : modelState.resetModelOverride ,
provider ,
model ,
initialModelLabel ,
formatModelSwitchEvent ,
2026-01-07 12:21:20 +01:00
currentThinkLevel ,
2026-01-08 03:22:14 +01:00
currentVerboseLevel ,
currentReasoningLevel ,
currentElevatedLevel ,
2026-01-04 05:47:21 +01:00
} ) ;
2026-01-09 03:46:00 +01:00
let statusReply : ReplyPayload | undefined ;
2026-01-12 06:10:17 +00:00
if (
directives . hasStatusDirective &&
allowTextCommands &&
command . isAuthorizedSender
) {
2026-01-09 03:46:00 +01:00
statusReply = await buildStatusReply ( {
cfg ,
command ,
sessionEntry ,
sessionKey ,
sessionScope ,
provider ,
model ,
contextTokens ,
2026-01-12 03:00:27 +00:00
resolvedThinkLevel : resolvedDefaultThinkLevel ,
2026-01-09 03:46:00 +01:00
resolvedVerboseLevel : ( currentVerboseLevel ? ? "off" ) as VerboseLevel ,
resolvedReasoningLevel : ( currentReasoningLevel ? ?
"off" ) as ReasoningLevel ,
2026-01-09 20:42:16 +00:00
resolvedElevatedLevel ,
2026-01-12 03:00:27 +00:00
resolveDefaultThinkingLevel : async ( ) = > resolvedDefaultThinkLevel ,
2026-01-09 03:46:00 +01:00
isGroup ,
defaultGroupActivation : ( ) = > defaultActivation ,
} ) ;
}
2026-01-04 05:47:21 +01:00
typing . cleanup ( ) ;
2026-01-09 03:46:00 +01:00
if ( statusReply ? . text && directiveReply ? . text ) {
return { text : ` ${ directiveReply . text } \ n ${ statusReply . text } ` } ;
}
return statusReply ? ? directiveReply ;
2025-12-03 09:09:34 +00:00
}
2025-12-03 09:04:37 +00:00
2026-01-12 22:33:59 +00:00
const hasAnyDirective =
directives . hasThinkDirective ||
directives . hasVerboseDirective ||
directives . hasReasoningDirective ||
directives . hasElevatedDirective ||
directives . hasModelDirective ||
directives . hasQueueDirective ||
directives . hasStatusDirective ;
if ( hasAnyDirective && command . isAuthorizedSender ) {
const fastLane = await applyInlineDirectivesFastLane ( {
directives ,
commandAuthorized : command.isAuthorizedSender ,
ctx ,
cfg ,
agentId ,
isGroup ,
sessionEntry ,
sessionStore ,
sessionKey ,
storePath ,
elevatedEnabled ,
elevatedAllowed ,
elevatedFailures ,
messageProviderKey ,
defaultProvider ,
defaultModel ,
aliasIndex ,
allowedModelKeys : modelState.allowedModelKeys ,
allowedModelCatalog : modelState.allowedModelCatalog ,
resetModelOverride : modelState.resetModelOverride ,
provider ,
model ,
initialModelLabel ,
formatModelSwitchEvent ,
agentCfg ,
modelState : {
resolveDefaultThinkingLevel : modelState.resolveDefaultThinkingLevel ,
allowedModelKeys : modelState.allowedModelKeys ,
allowedModelCatalog : modelState.allowedModelCatalog ,
resetModelOverride : modelState.resetModelOverride ,
} ,
} ) ;
directiveAck = fastLane . directiveAck ;
provider = fastLane . provider ;
model = fastLane . model ;
}
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-09 15:20:03 +01:00
agentDir ,
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-12 06:10:17 +00:00
const sendInlineReply = async ( reply? : ReplyPayload ) = > {
if ( ! reply ) return ;
if ( ! opts ? . onBlockReply ) return ;
await opts . onBlockReply ( reply ) ;
} ;
const inlineCommand =
allowTextCommands && command . isAuthorizedSender
? extractInlineSimpleCommand ( cleanedBody )
: null ;
if ( inlineCommand ) {
cleanedBody = inlineCommand . cleaned ;
sessionCtx . Body = cleanedBody ;
sessionCtx . BodyStripped = cleanedBody ;
}
const handleInlineStatus =
! isDirectiveOnly ( {
directives ,
cleanedBody : directives.cleaned ,
ctx ,
cfg ,
agentId ,
isGroup ,
2026-01-12 11:22:56 +00:00
} ) && inlineStatusRequested ;
2026-01-12 06:10:17 +00:00
if ( handleInlineStatus ) {
const inlineStatusReply = await buildStatusReply ( {
cfg ,
command ,
sessionEntry ,
sessionKey ,
sessionScope ,
provider ,
model ,
contextTokens ,
resolvedThinkLevel ,
resolvedVerboseLevel : resolvedVerboseLevel ? ? "off" ,
resolvedReasoningLevel ,
resolvedElevatedLevel ,
resolveDefaultThinkingLevel : modelState.resolveDefaultThinkingLevel ,
isGroup ,
defaultGroupActivation : ( ) = > defaultActivation ,
} ) ;
await sendInlineReply ( inlineStatusReply ) ;
directives = { . . . directives , hasStatusDirective : false } ;
}
if ( inlineCommand ) {
const inlineCommandContext = {
. . . command ,
rawBodyNormalized : inlineCommand.command ,
commandBodyNormalized : inlineCommand.command ,
} ;
const inlineResult = await handleCommands ( {
ctx ,
cfg ,
command : inlineCommandContext ,
agentId ,
directives ,
2026-01-12 15:04:57 +05:30
elevated : {
enabled : elevatedEnabled ,
allowed : elevatedAllowed ,
failures : elevatedFailures ,
} ,
2026-01-12 06:10:17 +00:00
sessionEntry ,
sessionStore ,
sessionKey ,
storePath ,
sessionScope ,
workspaceDir ,
defaultGroupActivation : ( ) = > defaultActivation ,
resolvedThinkLevel ,
resolvedVerboseLevel : resolvedVerboseLevel ? ? "off" ,
resolvedReasoningLevel ,
resolvedElevatedLevel ,
resolveDefaultThinkingLevel : modelState.resolveDefaultThinkingLevel ,
provider ,
model ,
contextTokens ,
isGroup ,
} ) ;
if ( inlineResult . reply ) {
if ( ! inlineCommand . cleaned ) {
typing . cleanup ( ) ;
return inlineResult . reply ;
}
await sendInlineReply ( inlineResult . reply ) ;
}
}
2026-01-12 21:44:09 +00:00
if ( directiveAck ) {
await sendInlineReply ( directiveAck ) ;
}
2026-01-02 17:15:12 +01:00
const isEmptyConfig = Object . keys ( cfg ) . length === 0 ;
2026-01-11 11:45:25 +00:00
const skipWhenConfigEmpty = command . providerId
? Boolean (
getProviderDock ( command . providerId ) ? . commands ? . skipWhenConfigEmpty ,
)
: false ;
2025-12-07 16:53:19 +00:00
if (
2026-01-11 11:45:25 +00:00
skipWhenConfigEmpty &&
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-08 22:57:08 +01:00
agentId ,
2026-01-05 01:31:36 +01:00
directives ,
2026-01-12 15:04:57 +05:30
elevated : {
enabled : elevatedEnabled ,
allowed : elevatedAllowed ,
failures : elevatedFailures ,
} ,
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 ;
2026-01-07 21:58:54 +00:00
const typingMode = resolveTypingMode ( {
2026-01-07 22:18:11 +00:00
configured : sessionCfg?.typingMode ? ? agentCfg ? . typingMode ,
2026-01-07 21:58:54 +00:00
isGroupChat ,
wasMentioned ,
isHeartbeat ,
} ) ;
2026-01-07 22:18:11 +00:00
const typingSignals = createTypingSignaler ( {
typing ,
mode : typingMode ,
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 ( {
2026-01-11 11:45:25 +00:00
cfg ,
2026-01-04 05:47:21 +01:00
sessionCtx ,
sessionEntry ,
defaultActivation ,
silentToken : SILENT_REPLY_TOKEN ,
} )
2025-12-24 00:33:35 +00:00
: "" ;
2026-01-07 11:22:55 +01:00
const groupSystemPrompt = sessionCtx . GroupSystemPrompt ? . trim ( ) ? ? "" ;
const extraSystemPrompt = [ groupIntro , groupSystemPrompt ]
. filter ( Boolean )
. join ( "\n\n" ) ;
2025-11-26 00:53:53 +01:00
const baseBody = sessionCtx . BodyStripped ? ? sessionCtx . Body ? ? "" ;
2026-01-10 18:53:33 +01:00
// Use CommandBody/RawBody for bare reset detection (clean message without structural context).
const rawBodyTrimmed = (
ctx . CommandBody ? ?
ctx . RawBody ? ?
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 &&
2026-01-12 06:10:17 +00:00
( ! commandAuthorized || ! command . isAuthorizedSender ) &&
2026-01-06 14:17:56 -06:00
! baseBodyTrimmedRaw &&
2026-01-11 02:17:10 +01:00
hasControlCommand ( commandSource , cfg )
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 =
2026-01-09 22:30:10 +01:00
! isGroupSession && sessionKey === normalizeMainKey ( sessionCfg ? . mainKey ) ;
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 ,
} ) ;
2026-01-07 09:02:20 -06:00
const threadStarterBody = ctx . ThreadStarterBody ? . trim ( ) ;
const threadStarterNote =
isNewSession && threadStarterBody
? ` [Thread starter - for context] \ n ${ threadStarterBody } `
: undefined ;
2026-01-04 05:47:21 +01:00
const skillResult = await ensureSkillSnapshot ( {
sessionEntry ,
sessionStore ,
sessionKey ,
storePath ,
sessionId ,
isFirstTurnInSession ,
workspaceDir ,
cfg ,
2026-01-07 11:22:55 +01:00
skillFilter : opts?.skillFilter ,
2026-01-04 05:47:21 +01:00
} ) ;
sessionEntry = skillResult . sessionEntry ? ? sessionEntry ;
systemSent = skillResult . systemSent ;
const skillsSnapshot = skillResult . skillsSnapshot ;
2025-12-17 11:29:04 +01:00
const prefixedBody = transcribedText
2026-01-07 09:02:20 -06:00
? [ threadStarterNote , prefixedBodyBase , ` Transcript: \ n ${ transcribedText } ` ]
2025-12-17 11:29:04 +01:00
. filter ( Boolean )
. join ( "\n\n" )
2026-01-07 09:02:20 -06:00
: [ threadStarterNote , prefixedBodyBase ] . filter ( Boolean ) . join ( "\n\n" ) ;
2026-01-08 05:20:04 +01:00
const mediaNote = buildInboundMediaNote ( ctx ) ;
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 ;
2026-01-10 18:53:33 +01:00
let prefixedCommandBody = mediaNote
2025-11-26 00:53:53 +01:00
? [ mediaNote , mediaReplyHint , prefixedBody ? ? "" ]
. filter ( Boolean )
. join ( "\n" )
. trim ( )
: prefixedBody ;
2026-01-10 18:53:33 +01:00
if ( ! resolvedThinkLevel && prefixedCommandBody ) {
const parts = prefixedCommandBody . split ( /\s+/ ) ;
2025-12-03 08:45:23 +00:00
const maybeLevel = normalizeThinkLevel ( parts [ 0 ] ) ;
2026-01-07 17:17:38 -08:00
if (
maybeLevel &&
( maybeLevel !== "xhigh" || supportsXHighThinking ( provider , model ) )
) {
2025-12-03 08:45:23 +00:00
resolvedThinkLevel = maybeLevel ;
2026-01-10 18:53:33 +01:00
prefixedCommandBody = parts . slice ( 1 ) . join ( " " ) . trim ( ) ;
2025-12-03 08:45:23 +00:00
}
}
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
}
2026-01-07 17:17:38 -08:00
if (
resolvedThinkLevel === "xhigh" &&
! supportsXHighThinking ( provider , model )
) {
const explicitThink =
directives . hasThinkDirective && directives . thinkLevel !== undefined ;
if ( explicitThink ) {
typing . cleanup ( ) ;
return {
text : ` Thinking level "xhigh" is only supported for ${ formatXHighModelHint ( ) } . Use /think high or switch to one of those models. ` ,
} ;
}
resolvedThinkLevel = "high" ;
if (
sessionEntry &&
sessionStore &&
sessionKey &&
sessionEntry . thinkingLevel === "xhigh"
) {
sessionEntry . thinkingLevel = "high" ;
sessionEntry . updatedAt = Date . now ( ) ;
sessionStore [ sessionKey ] = sessionEntry ;
if ( storePath ) {
await saveSessionStore ( storePath , sessionStore ) ;
}
}
}
2025-12-17 11:29:04 +01:00
const sessionIdFinal = sessionId ? ? crypto . randomUUID ( ) ;
2026-01-07 22:56:50 +00:00
const sessionFile = resolveSessionFilePath ( sessionIdFinal , sessionEntry ) ;
2025-12-20 16:10:46 +01:00
const queueBodyBase = transcribedText
2026-01-07 09:02:20 -06:00
? [ threadStarterNote , baseBodyFinal , ` Transcript: \ n ${ transcribedText } ` ]
2025-12-20 16:10:46 +01:00
. filter ( Boolean )
. join ( "\n\n" )
2026-01-07 09:02:20 -06:00
: [ threadStarterNote , baseBodyFinal ] . filter ( Boolean ) . join ( "\n\n" ) ;
2025-12-20 16:10:46 +01:00
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 ,
2026-01-09 20:44:11 +01:00
messageId : sessionCtx.MessageSid ,
2026-01-03 04:26:36 +01:00
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-08 08:49:16 +01:00
agentAccountId : sessionCtx.AccountId ,
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-07 11:22:55 +01:00
extraSystemPrompt : extraSystemPrompt || undefined ,
2026-01-13 09:47:04 +13:00
. . . ( isReasoningTagProvider ( provider ) ? { enforceFinalTag : true } : { } ) ,
2026-01-03 04:26:36 +01:00
} ,
} ;
2026-01-07 22:18:11 +00:00
if ( typingSignals . shouldStartImmediately ) {
await typingSignals . signalRunStart ( ) ;
2026-01-04 05:47:21 +01:00
}
return runReplyAgent ( {
2026-01-10 18:53:33 +01:00
commandBody : prefixedCommandBody ,
2026-01-04 05:47:21 +01:00
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 ,
2026-01-07 21:58:54 +00:00
typingMode ,
2026-01-04 05:47:21 +01:00
} ) ;
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 ;
2026-01-08 04:44:11 +00:00
const hasPathsArray =
Array . isArray ( ctx . MediaPaths ) && ctx . MediaPaths . length > 0 ;
2026-01-08 04:40:50 +00:00
const pathsFromArray = Array . isArray ( ctx . MediaPaths )
2026-01-08 05:20:04 +01:00
? ctx . MediaPaths
2026-01-08 04:40:50 +00:00
: undefined ;
2026-01-08 04:44:11 +00:00
const rawPaths =
pathsFromArray && pathsFromArray . length > 0
? pathsFromArray
: ctx . MediaPath ? . trim ( )
? [ ctx . MediaPath . trim ( ) ]
: [ ] ;
2026-01-08 05:20:04 +01:00
if ( rawPaths . length === 0 || ! sessionKey ) return ;
2026-01-05 06:18:11 +01:00
const sandbox = await ensureSandboxWorkspaceForSession ( {
config : cfg ,
sessionKey ,
workspaceDir ,
} ) ;
if ( ! sandbox ) return ;
2026-01-08 05:20:04 +01:00
const resolveAbsolutePath = ( value : string ) : string | null = > {
let resolved = value . trim ( ) ;
if ( ! resolved ) return null ;
if ( resolved . startsWith ( "file://" ) ) {
try {
resolved = fileURLToPath ( resolved ) ;
} catch {
return null ;
}
2026-01-05 06:18:11 +01:00
}
2026-01-08 05:20:04 +01:00
if ( ! path . isAbsolute ( resolved ) ) return null ;
return resolved ;
} ;
2026-01-05 06:18:11 +01:00
try {
const destDir = path . join ( sandbox . workspaceDir , "media" , "inbound" ) ;
await fs . mkdir ( destDir , { recursive : true } ) ;
2026-01-08 05:20:04 +01:00
const usedNames = new Set < string > ( ) ;
const staged = new Map < string , string > ( ) ; // absolute source -> relative sandbox path
for ( const raw of rawPaths ) {
const source = resolveAbsolutePath ( raw ) ;
if ( ! source ) continue ;
if ( staged . has ( source ) ) continue ;
const baseName = path . basename ( source ) ;
if ( ! baseName ) continue ;
const parsed = path . parse ( baseName ) ;
let fileName = baseName ;
let suffix = 1 ;
while ( usedNames . has ( fileName ) ) {
fileName = ` ${ parsed . name } - ${ suffix } ${ parsed . ext } ` ;
suffix += 1 ;
2026-01-05 06:18:11 +01:00
}
2026-01-08 05:20:04 +01:00
usedNames . add ( fileName ) ;
const dest = path . join ( destDir , fileName ) ;
await fs . copyFile ( source , dest ) ;
const relative = path . posix . join ( "media" , "inbound" , fileName ) ;
staged . set ( source , relative ) ;
}
const rewriteIfStaged = ( value : string | undefined ) : string | undefined = > {
const raw = value ? . trim ( ) ;
if ( ! raw ) return value ;
const abs = resolveAbsolutePath ( raw ) ;
if ( ! abs ) return value ;
const mapped = staged . get ( abs ) ;
return mapped ? ? value ;
} ;
const nextMediaPaths = hasPathsArray
? rawPaths . map ( ( p ) = > rewriteIfStaged ( p ) ? ? p )
: undefined ;
if ( nextMediaPaths ) {
ctx . MediaPaths = nextMediaPaths ;
sessionCtx . MediaPaths = nextMediaPaths ;
ctx . MediaPath = nextMediaPaths [ 0 ] ;
sessionCtx . MediaPath = nextMediaPaths [ 0 ] ;
} else {
const rewritten = rewriteIfStaged ( ctx . MediaPath ) ;
if ( rewritten && rewritten !== ctx . MediaPath ) {
ctx . MediaPath = rewritten ;
sessionCtx . MediaPath = rewritten ;
2026-01-05 06:18:11 +01:00
}
}
2026-01-08 05:20:04 +01:00
if ( Array . isArray ( ctx . MediaUrls ) && ctx . MediaUrls . length > 0 ) {
const nextUrls = ctx . MediaUrls . map ( ( u ) = > rewriteIfStaged ( u ) ? ? u ) ;
ctx . MediaUrls = nextUrls ;
sessionCtx . MediaUrls = nextUrls ;
}
const rewrittenUrl = rewriteIfStaged ( ctx . MediaUrl ) ;
if ( rewrittenUrl && rewrittenUrl !== ctx . MediaUrl ) {
ctx . MediaUrl = rewrittenUrl ;
sessionCtx . MediaUrl = rewrittenUrl ;
}
2026-01-05 06:18:11 +01:00
} catch ( err ) {
logVerbose ( ` Failed to stage inbound media for sandbox: ${ String ( err ) } ` ) ;
}
}