2025-12-22 20:45:22 +00:00
import fs from "node:fs/promises" ;
import os from "node:os" ;
2026-01-07 12:02:46 +01:00
import path from "node:path" ;
import { fileURLToPath } from "node:url" ;
2025-12-22 20:45:22 +00:00
2026-01-07 06:12:56 +00:00
import type {
AgentMessage ,
AgentTool ,
2026-01-11 15:56:43 +00:00
StreamFn ,
2026-01-07 06:12:56 +00:00
ThinkingLevel ,
} from "@mariozechner/pi-agent-core" ;
2026-01-10 19:17:32 +02:00
import type {
Api ,
AssistantMessage ,
ImageContent ,
Model ,
2026-01-11 15:56:43 +00:00
SimpleStreamOptions ,
2026-01-10 19:17:32 +02:00
} from "@mariozechner/pi-ai" ;
2026-01-11 15:56:43 +00:00
import { streamSimple } from "@mariozechner/pi-ai" ;
2025-12-22 20:45:22 +00:00
import {
createAgentSession ,
2025-12-26 10:16:50 +01:00
discoverAuthStorage ,
discoverModels ,
2025-12-22 20:45:22 +00:00
SessionManager ,
SettingsManager ,
} from "@mariozechner/pi-coding-agent" ;
2026-01-06 21:54:19 +00:00
import { resolveHeartbeatPrompt } from "../auto-reply/heartbeat.js" ;
2026-01-10 03:01:04 +01:00
import { parseReplyDirectives } from "../auto-reply/reply/reply-directives.js" ;
2026-01-07 06:16:38 +01:00
import type {
ReasoningLevel ,
ThinkLevel ,
VerboseLevel ,
} from "../auto-reply/thinking.js" ;
2025-12-22 20:45:22 +00:00
import { formatToolAggregate } from "../auto-reply/tool-meta.js" ;
2026-01-07 22:56:50 +00:00
import { isCacheEnabled , resolveCacheTtlMs } from "../config/cache-utils.js" ;
2026-01-04 14:32:47 +00:00
import type { ClawdbotConfig } from "../config/config.js" ;
2026-01-09 20:46:11 +01:00
import { resolveProviderCapabilities } from "../config/provider-capabilities.js" ;
2025-12-23 14:05:43 +00:00
import { getMachineDisplayName } from "../infra/machine-name.js" ;
2026-01-12 00:28:02 +00:00
import { registerUnhandledRejectionHandler } from "../infra/unhandled-rejections.js" ;
2026-01-03 21:09:44 +00:00
import { createSubsystemLogger } from "../logging.js" ;
2025-12-25 23:50:52 +01:00
import {
2025-12-25 23:58:37 +00:00
type enqueueCommand ,
2025-12-25 23:50:52 +01:00
enqueueCommandInLane ,
} from "../process/command-queue.js" ;
2026-01-09 20:46:11 +01:00
import { normalizeMessageProvider } from "../utils/message-provider.js" ;
2026-01-12 15:50:32 +13:00
import { isReasoningTagProvider } from "../utils/provider-utils.js" ;
2026-01-04 19:08:13 +01:00
import { resolveUserPath } from "../utils.js" ;
2026-01-04 14:32:47 +00:00
import { resolveClawdbotAgentDir } from "./agent-paths.js" ;
2026-01-10 01:57:33 +01:00
import { resolveSessionAgentIds } from "./agent-scope.js" ;
2026-01-06 07:18:06 +01:00
import {
2026-01-09 21:31:13 +01:00
markAuthProfileFailure ,
2026-01-06 07:18:06 +01:00
markAuthProfileGood ,
markAuthProfileUsed ,
} from "./auth-profiles.js" ;
2026-01-12 02:49:55 +00:00
import type { ExecElevatedDefaults , ExecToolDefaults } from "./bash-tools.js" ;
2026-01-10 00:47:34 +01:00
import {
CONTEXT_WINDOW_HARD_MIN_TOKENS ,
CONTEXT_WINDOW_WARN_BELOW_TOKENS ,
evaluateContextWindowGuard ,
resolveContextWindowInfo ,
} from "./context-window-guard.js" ;
2026-01-07 12:02:46 +01:00
import {
DEFAULT_CONTEXT_TOKENS ,
DEFAULT_MODEL ,
DEFAULT_PROVIDER ,
} from "./defaults.js" ;
2026-01-09 21:57:52 +01:00
import { FailoverError , resolveFailoverStatus } from "./failover-error.js" ;
2026-01-06 00:56:29 +00:00
import {
ensureAuthProfileStore ,
getApiKeyForModel ,
resolveAuthProfileOrder ,
2026-01-10 03:05:56 +00:00
resolveModelAuthMode ,
2026-01-06 00:56:29 +00:00
} from "./model-auth.js" ;
2026-01-11 04:28:16 +01:00
import { normalizeModelCompat } from "./model-compat.js" ;
2026-01-04 14:32:47 +00:00
import { ensureClawdbotModelsJson } from "./models-config.js" ;
2026-01-10 16:02:47 +01:00
import type { MessagingToolSend } from "./pi-embedded-messaging.js" ;
2026-01-12 05:28:17 +00:00
import {
ensurePiCompactionReserveTokens ,
resolveCompactionReserveTokensFloor ,
} from "./pi-settings.js" ;
2026-01-11 02:24:25 +00:00
import { acquireSessionWriteLock } from "./session-write-lock.js" ;
2026-01-10 16:02:47 +01:00
export type { MessagingToolSend } from "./pi-embedded-messaging.js" ;
2025-12-22 20:45:22 +00:00
import {
buildBootstrapContextFiles ,
2026-01-09 21:31:13 +01:00
classifyFailoverReason ,
2026-01-13 01:03:10 +00:00
downgradeGeminiHistory ,
2026-01-08 02:20:18 +01:00
type EmbeddedContextFile ,
2025-12-22 20:45:22 +00:00
ensureSessionHeader ,
formatAssistantErrorText ,
2026-01-06 00:56:29 +00:00
isAuthAssistantError ,
2026-01-08 18:34:08 -06:00
isCloudCodeAssistFormatError ,
2026-01-12 00:28:02 +00:00
isCompactionFailureError ,
2026-01-07 07:51:04 -06:00
isContextOverflowError ,
2026-01-09 21:31:13 +01:00
isFailoverAssistantError ,
isFailoverErrorMessage ,
2026-01-07 22:04:53 +01:00
isGoogleModelApi ,
2026-01-05 18:04:36 +01:00
isRateLimitAssistantError ,
2026-01-09 21:31:13 +01:00
isTimeoutErrorMessage ,
2026-01-05 18:54:23 +01:00
pickFallbackThinkingLevel ,
2026-01-07 18:30:35 +01:00
sanitizeGoogleTurnOrdering ,
2025-12-22 20:45:22 +00:00
sanitizeSessionMessagesImages ,
2026-01-12 16:41:10 -05:00
validateAnthropicTurns ,
2026-01-07 22:37:16 +08:00
validateGeminiTurns ,
2025-12-22 20:45:22 +00:00
} from "./pi-embedded-helpers.js" ;
2026-01-03 16:45:53 +01:00
import {
type BlockReplyChunking ,
2026-01-03 12:35:16 -06:00
subscribeEmbeddedPiSession ,
2026-01-03 16:45:53 +01:00
} from "./pi-embedded-subscribe.js" ;
2026-01-07 06:16:38 +01:00
import {
extractAssistantText ,
extractAssistantThinking ,
2026-01-10 00:53:19 +01:00
formatReasoningMessage ,
2026-01-07 06:16:38 +01:00
} from "./pi-embedded-utils.js" ;
2026-01-07 12:02:46 +01:00
import { setContextPruningRuntime } from "./pi-extensions/context-pruning/runtime.js" ;
import { computeEffectiveSettings } from "./pi-extensions/context-pruning/settings.js" ;
import { makeToolPrunablePredicate } from "./pi-extensions/context-pruning/tools.js" ;
2026-01-05 18:05:40 +00:00
import { toToolDefinitions } from "./pi-tool-definition-adapter.js" ;
2026-01-04 14:32:47 +00:00
import { createClawdbotCodingTools } from "./pi-tools.js" ;
2026-01-03 21:30:40 +00:00
import { resolveSandboxContext } from "./sandbox.js" ;
2026-01-12 17:48:08 +00:00
import { guardSessionManager } from "./session-tool-result-guard-wrapper.js" ;
2026-01-10 22:03:42 +00:00
import { sanitizeToolUseResultPairing } from "./session-transcript-repair.js" ;
2025-12-22 20:45:22 +00:00
import {
applySkillEnvOverrides ,
applySkillEnvOverridesFromSnapshot ,
loadWorkspaceSkillEntries ,
2026-01-09 21:27:11 +01:00
resolveSkillsPromptForRun ,
2025-12-22 20:45:22 +00:00
type SkillSnapshot ,
} from "./skills.js" ;
2026-01-08 02:20:18 +01:00
import { buildAgentSystemPrompt } from "./system-prompt.js" ;
2026-01-11 11:45:25 +00:00
import { buildToolSummaryMap } from "./tool-summaries.js" ;
2026-01-06 18:51:45 +00:00
import { normalizeUsage , type UsageLike } from "./usage.js" ;
2026-01-10 00:01:16 +00:00
import {
filterBootstrapFilesForSession ,
loadWorkspaceBootstrapFiles ,
} from "./workspace.js" ;
2025-12-22 20:45:22 +00:00
2026-01-07 12:02:46 +01:00
// Optional features can be implemented as Pi extensions that run in the same Node process.
2026-01-07 23:57:40 -03:00
/ * *
* Resolve provider - specific extraParams from model config .
* Auto - enables thinking mode for GLM - 4 . x models unless explicitly disabled .
*
* For ZAI GLM - 4 . x models , we auto - enable thinking via the Z . AI Cloud API format :
* thinking : { type : "enabled" , clear_thinking : boolean }
*
* - GLM - 4.7 : Preserved thinking ( clear_thinking : false ) - reasoning kept across turns
* - GLM - 4.5 / 4.6 : Interleaved thinking ( clear_thinking : true ) - reasoning cleared each turn
*
* Users can override via config :
2026-01-09 12:44:23 +00:00
* agents . defaults . models [ "zai/glm-4.7" ] . params . thinking = { type : "disabled" }
2026-01-07 23:57:40 -03:00
*
* Or disable via runtime flag : -- thinking off
*
* @see https : //docs.z.ai/guides/capabilities/thinking-mode
* @internal Exported for testing only
* /
export function resolveExtraParams ( params : {
cfg : ClawdbotConfig | undefined ;
provider : string ;
modelId : string ;
thinkLevel? : string ;
} ) : Record < string , unknown > | undefined {
const modelKey = ` ${ params . provider } / ${ params . modelId } ` ;
2026-01-09 12:44:23 +00:00
const modelConfig = params . cfg ? . agents ? . defaults ? . models ? . [ modelKey ] ;
2026-01-07 23:57:40 -03:00
let extraParams = modelConfig ? . params ? { . . . modelConfig . params } : undefined ;
// Auto-enable thinking for ZAI GLM-4.x models when not explicitly configured
// Skip if user explicitly disabled thinking via --thinking off
if ( params . provider === "zai" && params . thinkLevel !== "off" ) {
const modelIdLower = params . modelId . toLowerCase ( ) ;
const isGlm4 = modelIdLower . includes ( "glm-4" ) ;
if ( isGlm4 ) {
// Check if user has explicitly configured thinking params
const hasThinkingConfig = extraParams ? . thinking !== undefined ;
if ( ! hasThinkingConfig ) {
// GLM-4.7 supports preserved thinking (reasoning kept across turns)
// GLM-4.5/4.6 use interleaved thinking (reasoning cleared each turn)
// Z.AI Cloud API format: thinking: { type: "enabled", clear_thinking: boolean }
const isGlm47 = modelIdLower . includes ( "glm-4.7" ) ;
const clearThinking = ! isGlm47 ;
extraParams = {
. . . extraParams ,
thinking : {
type : "enabled" ,
clear_thinking : clearThinking ,
} ,
} ;
log . debug (
` auto-enabled thinking for ${ modelKey } : type=enabled, clear_thinking= ${ clearThinking } ` ,
) ;
}
}
}
return extraParams ;
}
2026-01-11 15:56:43 +00:00
/ * *
* Create a wrapped streamFn that injects extra params ( like temperature ) from config .
*
* @internal
* /
function createStreamFnWithExtraParams (
2026-01-11 23:55:14 +00:00
baseStreamFn : StreamFn | undefined ,
2026-01-11 15:56:43 +00:00
extraParams : Record < string , unknown > | undefined ,
) : StreamFn | undefined {
if ( ! extraParams || Object . keys ( extraParams ) . length === 0 ) {
return undefined ; // No wrapper needed
}
const streamParams : Partial < SimpleStreamOptions > = { } ;
if ( typeof extraParams . temperature === "number" ) {
streamParams . temperature = extraParams . temperature ;
}
if ( typeof extraParams . maxTokens === "number" ) {
streamParams . maxTokens = extraParams . maxTokens ;
}
if ( Object . keys ( streamParams ) . length === 0 ) {
return undefined ;
}
2026-01-11 15:58:10 +00:00
log . debug (
` creating streamFn wrapper with params: ${ JSON . stringify ( streamParams ) } ` ,
) ;
2026-01-11 15:56:43 +00:00
2026-01-11 23:55:14 +00:00
const underlying = baseStreamFn ? ? streamSimple ;
const wrappedStreamFn : StreamFn = ( model , context , options ) = >
underlying ( model , context , {
2026-01-11 15:56:43 +00:00
. . . streamParams ,
. . . options , // Caller options take precedence
2026-01-11 23:55:14 +00:00
} ) ;
2026-01-11 15:56:43 +00:00
return wrappedStreamFn ;
}
/ * *
* Apply extra params ( like temperature ) to an agent ' s streamFn .
*
2026-01-11 23:55:14 +00:00
* @internal Exported for testing
2026-01-11 15:56:43 +00:00
* /
2026-01-11 23:55:14 +00:00
export function applyExtraParamsToAgent (
agent : { streamFn? : StreamFn } ,
2026-01-11 15:56:43 +00:00
cfg : ClawdbotConfig | undefined ,
provider : string ,
modelId : string ,
thinkLevel? : string ,
) : void {
2026-01-11 15:58:10 +00:00
const extraParams = resolveExtraParams ( {
cfg ,
provider ,
modelId ,
thinkLevel ,
} ) ;
2026-01-11 23:55:14 +00:00
const wrappedStreamFn = createStreamFnWithExtraParams (
agent . streamFn ,
extraParams ,
) ;
2026-01-11 15:56:43 +00:00
if ( wrappedStreamFn ) {
log . debug (
` applying extraParams to agent streamFn for ${ provider } / ${ modelId } ` ,
) ;
agent . streamFn = wrappedStreamFn ;
}
}
2026-01-07 12:02:46 +01:00
// We configure context pruning per-session via a WeakMap registry keyed by the SessionManager instance.
function resolvePiExtensionPath ( id : string ) : string {
const self = fileURLToPath ( import . meta . url ) ;
const dir = path . dirname ( self ) ;
// In dev this file is `.ts` (tsx), in production it's `.js`.
const ext = path . extname ( self ) === ".ts" ? "ts" : "js" ;
return path . join ( dir , "pi-extensions" , ` ${ id } . ${ ext } ` ) ;
}
function resolveContextWindowTokens ( params : {
cfg : ClawdbotConfig | undefined ;
provider : string ;
modelId : string ;
model : Model < Api > | undefined ;
} ) : number {
2026-01-10 00:47:34 +01:00
return resolveContextWindowInfo ( {
cfg : params.cfg ,
provider : params.provider ,
modelId : params.modelId ,
modelContextWindow : params.model?.contextWindow ,
defaultTokens : DEFAULT_CONTEXT_TOKENS ,
} ) . tokens ;
2026-01-07 12:02:46 +01:00
}
function buildContextPruningExtension ( params : {
cfg : ClawdbotConfig | undefined ;
sessionManager : SessionManager ;
provider : string ;
modelId : string ;
model : Model < Api > | undefined ;
} ) : { additionalExtensionPaths? : string [ ] } {
2026-01-09 12:44:23 +00:00
const raw = params . cfg ? . agents ? . defaults ? . contextPruning ;
2026-01-07 12:02:46 +01:00
if ( raw ? . mode !== "adaptive" && raw ? . mode !== "aggressive" ) return { } ;
const settings = computeEffectiveSettings ( raw ) ;
if ( ! settings ) return { } ;
setContextPruningRuntime ( params . sessionManager , {
settings ,
contextWindowTokens : resolveContextWindowTokens ( params ) ,
isToolPrunable : makeToolPrunablePredicate ( settings . tools ) ,
} ) ;
return {
additionalExtensionPaths : [ resolvePiExtensionPath ( "context-pruning" ) ] ,
} ;
}
2026-01-11 04:46:18 +00:00
function buildEmbeddedExtensionPaths ( params : {
cfg : ClawdbotConfig | undefined ;
sessionManager : SessionManager ;
provider : string ;
modelId : string ;
model : Model < Api > | undefined ;
} ) : string [ ] {
const paths = [ resolvePiExtensionPath ( "transcript-sanitize" ) ] ;
const pruning = buildContextPruningExtension ( params ) ;
if ( pruning . additionalExtensionPaths ) {
paths . push ( . . . pruning . additionalExtensionPaths ) ;
}
return paths ;
}
2025-12-22 20:45:22 +00:00
export type EmbeddedPiAgentMeta = {
sessionId : string ;
provider : string ;
model : string ;
usage ? : {
input? : number ;
output? : number ;
cacheRead? : number ;
cacheWrite? : number ;
total? : number ;
} ;
} ;
export type EmbeddedPiRunMeta = {
durationMs : number ;
agentMeta? : EmbeddedPiAgentMeta ;
aborted? : boolean ;
} ;
2026-01-07 06:53:01 +01:00
function buildModelAliasLines ( cfg? : ClawdbotConfig ) {
2026-01-09 12:44:23 +00:00
const models = cfg ? . agents ? . defaults ? . models ? ? { } ;
2026-01-07 06:53:01 +01:00
const entries : Array < { alias : string ; model : string } > = [ ] ;
for ( const [ keyRaw , entryRaw ] of Object . entries ( models ) ) {
const model = String ( keyRaw ? ? "" ) . trim ( ) ;
if ( ! model ) continue ;
const alias = String (
( entryRaw as { alias? : string } | undefined ) ? . alias ? ? "" ,
) . trim ( ) ;
if ( ! alias ) continue ;
entries . push ( { alias , model } ) ;
}
return entries
. sort ( ( a , b ) = > a . alias . localeCompare ( b . alias ) )
. map ( ( entry ) = > ` - ${ entry . alias } : ${ entry . model } ` ) ;
}
2026-01-06 01:08:36 +00:00
type ApiKeyInfo = {
apiKey : string ;
profileId? : string ;
source : string ;
} ;
2025-12-22 20:45:22 +00:00
export type EmbeddedPiRunResult = {
payloads? : Array < {
text? : string ;
mediaUrl? : string ;
mediaUrls? : string [ ] ;
2026-01-02 23:18:41 +01:00
replyToId? : string ;
2026-01-06 21:17:55 +01:00
isError? : boolean ;
2025-12-22 20:45:22 +00:00
} > ;
meta : EmbeddedPiRunMeta ;
2026-01-07 03:24:56 -03:00
// True if a messaging tool (telegram, whatsapp, discord, slack, sessions_send)
// successfully sent a message. Used to suppress agent's confirmation text.
didSendViaMessagingTool? : boolean ;
2026-01-08 00:50:29 +00:00
// Texts successfully sent via messaging tools during the run.
messagingToolSentTexts? : string [ ] ;
2026-01-08 08:49:16 +01:00
// Messaging tool targets that successfully sent a message during the run.
messagingToolSentTargets? : MessagingToolSend [ ] ;
2025-12-22 20:45:22 +00:00
} ;
2026-01-06 02:06:06 +01:00
export type EmbeddedPiCompactResult = {
ok : boolean ;
compacted : boolean ;
reason? : string ;
result ? : {
summary : string ;
firstKeptEntryId : string ;
tokensBefore : number ;
details? : unknown ;
} ;
} ;
2025-12-22 20:45:22 +00:00
type EmbeddedPiQueueHandle = {
queueMessage : ( text : string ) = > Promise < void > ;
isStreaming : ( ) = > boolean ;
2026-01-06 05:33:08 +01:00
isCompacting : ( ) = > boolean ;
2025-12-26 13:35:44 +01:00
abort : ( ) = > void ;
2025-12-22 20:45:22 +00:00
} ;
2026-01-03 20:57:32 +00:00
const log = createSubsystemLogger ( "agent/embedded" ) ;
2026-01-07 22:04:53 +01:00
const GOOGLE_TURN_ORDERING_CUSTOM_TYPE = "google-turn-ordering-bootstrap" ;
2026-01-12 00:28:02 +00:00
registerUnhandledRejectionHandler ( ( reason ) = > {
const message = describeUnknownError ( reason ) ;
if ( ! isCompactionFailureError ( message ) ) return false ;
log . error ( ` Auto-compaction failed (unhandled): ${ message } ` ) ;
return true ;
} ) ;
2026-01-07 22:04:53 +01:00
type CustomEntryLike = { type ? : unknown ; customType? : unknown } ;
function hasGoogleTurnOrderingMarker ( sessionManager : SessionManager ) : boolean {
try {
return sessionManager
. getEntries ( )
. some (
( entry ) = >
( entry as CustomEntryLike ) ? . type === "custom" &&
( entry as CustomEntryLike ) ? . customType ===
GOOGLE_TURN_ORDERING_CUSTOM_TYPE ,
) ;
} catch {
return false ;
}
}
function markGoogleTurnOrderingMarker ( sessionManager : SessionManager ) : void {
try {
sessionManager . appendCustomEntry ( GOOGLE_TURN_ORDERING_CUSTOM_TYPE , {
timestamp : Date.now ( ) ,
} ) ;
} catch {
// ignore marker persistence failures
}
}
export function applyGoogleTurnOrderingFix ( params : {
messages : AgentMessage [ ] ;
modelApi? : string | null ;
sessionManager : SessionManager ;
sessionId : string ;
warn ? : ( message : string ) = > void ;
} ) : { messages : AgentMessage [ ] ; didPrepend : boolean } {
if ( ! isGoogleModelApi ( params . modelApi ) ) {
return { messages : params.messages , didPrepend : false } ;
}
const first = params . messages [ 0 ] as
| { role? : unknown ; content? : unknown }
| undefined ;
if ( first ? . role !== "assistant" ) {
return { messages : params.messages , didPrepend : false } ;
}
const sanitized = sanitizeGoogleTurnOrdering ( params . messages ) ;
const didPrepend = sanitized !== params . messages ;
if ( didPrepend && ! hasGoogleTurnOrderingMarker ( params . sessionManager ) ) {
const warn = params . warn ? ? ( ( message : string ) = > log . warn ( message ) ) ;
warn (
` google turn ordering fixup: prepended user bootstrap (sessionId= ${ params . sessionId } ) ` ,
) ;
markGoogleTurnOrderingMarker ( params . sessionManager ) ;
}
return { messages : sanitized , didPrepend } ;
}
async function sanitizeSessionHistory ( params : {
messages : AgentMessage [ ] ;
modelApi? : string | null ;
sessionManager : SessionManager ;
sessionId : string ;
} ) : Promise < AgentMessage [ ] > {
const sanitizedImages = await sanitizeSessionMessagesImages (
params . messages ,
"session:history" ,
2026-01-10 19:15:52 +00:00
{
sanitizeToolCallIds : isGoogleModelApi ( params . modelApi ) ,
enforceToolCallLast : params.modelApi === "anthropic-messages" ,
} ,
2026-01-07 22:04:53 +01:00
) ;
2026-01-10 19:15:52 +00:00
const repairedTools = sanitizeToolUseResultPairing ( sanitizedImages ) ;
2026-01-13 01:03:10 +00:00
// Downgrade tool calls missing thought_signature if using Gemini
const downgraded = isGoogleModelApi ( params . modelApi )
? downgradeGeminiHistory ( repairedTools )
: repairedTools ;
2026-01-07 22:04:53 +01:00
return applyGoogleTurnOrderingFix ( {
2026-01-13 01:03:10 +00:00
messages : downgraded ,
2026-01-07 22:04:53 +01:00
modelApi : params.modelApi ,
sessionManager : params.sessionManager ,
sessionId : params.sessionId ,
} ) . messages ;
}
2026-01-03 20:57:32 +00:00
2026-01-11 08:21:14 -06:00
/ * *
* Limits conversation history to the last N user turns ( and their associated
* assistant responses ) . This reduces token usage for long - running DM sessions .
*
* @param messages - The full message history
* @param limit - Max number of user turns to keep ( undefined = no limit )
* @returns Messages trimmed to the last ` limit ` user turns
* /
export function limitHistoryTurns (
messages : AgentMessage [ ] ,
limit : number | undefined ,
) : AgentMessage [ ] {
if ( ! limit || limit <= 0 || messages . length === 0 ) return messages ;
// Count user messages from the end, find cutoff point
let userCount = 0 ;
let lastUserIndex = messages . length ;
for ( let i = messages . length - 1 ; i >= 0 ; i -- ) {
if ( messages [ i ] . role === "user" ) {
userCount ++ ;
if ( userCount > limit ) {
// We exceeded the limit; keep from the last valid user turn onwards
return messages . slice ( lastUserIndex ) ;
}
lastUserIndex = i ;
}
}
// Fewer than limit user turns, keep all
return messages ;
}
2026-01-11 08:38:19 -06:00
/ * *
2026-01-11 08:53:50 -06:00
* Extracts the provider name and user ID from a session key and looks up
* dmHistoryLimit from the provider config , with per - DM override support .
2026-01-11 08:38:19 -06:00
*
* Session key formats :
2026-01-11 08:53:50 -06:00
* - ` telegram:dm:123 ` → provider = telegram , userId = 123
* - ` agent:main:telegram:dm:123 ` → provider = telegram , userId = 123
*
* Resolution order :
* 1 . Per - DM override : provider.dms [ userId ] . historyLimit
* 2 . Provider default : provider . dmHistoryLimit
2026-01-11 08:38:19 -06:00
* /
export function getDmHistoryLimitFromSessionKey (
sessionKey : string | undefined ,
config : ClawdbotConfig | undefined ,
) : number | undefined {
if ( ! sessionKey || ! config ) return undefined ;
const parts = sessionKey . split ( ":" ) . filter ( Boolean ) ;
// Handle agent-prefixed keys: agent:<agentId>:<provider>:...
const providerParts =
parts . length >= 3 && parts [ 0 ] === "agent" ? parts . slice ( 2 ) : parts ;
const provider = providerParts [ 0 ] ? . toLowerCase ( ) ;
if ( ! provider ) return undefined ;
2026-01-11 08:53:50 -06:00
// Extract userId: format is provider:dm:userId or provider:dm:userId:...
// The userId may contain colons (e.g., email addresses), so join remaining parts
const kind = providerParts [ 1 ] ? . toLowerCase ( ) ;
const userId = providerParts . slice ( 2 ) . join ( ":" ) ;
2026-01-11 22:18:15 +05:30
if ( kind !== "dm" ) return undefined ;
2026-01-11 08:53:50 -06:00
// Helper to get limit with per-DM override support
const getLimit = (
providerConfig :
| {
dmHistoryLimit? : number ;
dms? : Record < string , { historyLimit ? : number } > ;
}
| undefined ,
) : number | undefined = > {
if ( ! providerConfig ) return undefined ;
// Check per-DM override first
if (
userId &&
kind === "dm" &&
providerConfig . dms ? . [ userId ] ? . historyLimit !== undefined
) {
return providerConfig . dms [ userId ] . historyLimit ;
}
// Fall back to provider default
return providerConfig . dmHistoryLimit ;
} ;
2026-01-11 08:38:19 -06:00
// Map provider to config key
switch ( provider ) {
case "telegram" :
2026-01-11 08:53:50 -06:00
return getLimit ( config . telegram ) ;
2026-01-11 08:38:19 -06:00
case "whatsapp" :
2026-01-11 08:53:50 -06:00
return getLimit ( config . whatsapp ) ;
2026-01-11 08:38:19 -06:00
case "discord" :
2026-01-11 08:53:50 -06:00
return getLimit ( config . discord ) ;
2026-01-11 08:38:19 -06:00
case "slack" :
2026-01-11 08:53:50 -06:00
return getLimit ( config . slack ) ;
2026-01-11 08:38:19 -06:00
case "signal" :
2026-01-11 08:53:50 -06:00
return getLimit ( config . signal ) ;
2026-01-11 08:38:19 -06:00
case "imessage" :
2026-01-11 08:53:50 -06:00
return getLimit ( config . imessage ) ;
2026-01-11 08:38:19 -06:00
case "msteams" :
2026-01-11 08:53:50 -06:00
return getLimit ( config . msteams ) ;
2026-01-11 08:38:19 -06:00
default :
return undefined ;
}
}
2025-12-22 20:45:22 +00:00
const ACTIVE_EMBEDDED_RUNS = new Map < string , EmbeddedPiQueueHandle > ( ) ;
2026-01-03 23:57:17 +00:00
type EmbeddedRunWaiter = {
resolve : ( ended : boolean ) = > void ;
timer : NodeJS.Timeout ;
} ;
const EMBEDDED_RUN_WAITERS = new Map < string , Set < EmbeddedRunWaiter > > ( ) ;
2025-12-22 20:45:22 +00:00
2026-01-07 22:10:39 +08:00
// ============================================================================
// SessionManager Pre-warming Cache
// ============================================================================
type SessionManagerCacheEntry = {
sessionFile : string ;
loadedAt : number ;
} ;
const SESSION_MANAGER_CACHE = new Map < string , SessionManagerCacheEntry > ( ) ;
const DEFAULT_SESSION_MANAGER_TTL_MS = 45 _000 ; // 45 seconds
function getSessionManagerTtl ( ) : number {
2026-01-07 22:56:50 +00:00
return resolveCacheTtlMs ( {
envValue : process.env.CLAWDBOT_SESSION_MANAGER_CACHE_TTL_MS ,
defaultTtlMs : DEFAULT_SESSION_MANAGER_TTL_MS ,
} ) ;
2026-01-07 22:10:39 +08:00
}
function isSessionManagerCacheEnabled ( ) : boolean {
2026-01-07 22:56:50 +00:00
return isCacheEnabled ( getSessionManagerTtl ( ) ) ;
2026-01-07 22:10:39 +08:00
}
function trackSessionManagerAccess ( sessionFile : string ) : void {
if ( ! isSessionManagerCacheEnabled ( ) ) return ;
const now = Date . now ( ) ;
SESSION_MANAGER_CACHE . set ( sessionFile , {
sessionFile ,
loadedAt : now ,
} ) ;
}
function isSessionManagerCached ( sessionFile : string ) : boolean {
if ( ! isSessionManagerCacheEnabled ( ) ) return false ;
const entry = SESSION_MANAGER_CACHE . get ( sessionFile ) ;
if ( ! entry ) return false ;
const now = Date . now ( ) ;
const ttl = getSessionManagerTtl ( ) ;
return now - entry . loadedAt <= ttl ;
}
async function prewarmSessionFile ( sessionFile : string ) : Promise < void > {
if ( ! isSessionManagerCacheEnabled ( ) ) return ;
if ( isSessionManagerCached ( sessionFile ) ) return ;
try {
2026-01-07 22:38:41 +00:00
// Read a small chunk to encourage OS page cache warmup.
const handle = await fs . open ( sessionFile , "r" ) ;
try {
const buffer = Buffer . alloc ( 4096 ) ;
await handle . read ( buffer , 0 , buffer . length , 0 ) ;
} finally {
await handle . close ( ) ;
}
2026-01-07 22:10:39 +08:00
trackSessionManagerAccess ( sessionFile ) ;
} catch {
// File doesn't exist yet, SessionManager will create it
}
}
2026-01-07 01:00:47 +01:00
const isAbortError = ( err : unknown ) : boolean = > {
if ( ! err || typeof err !== "object" ) return false ;
const name = "name" in err ? String ( err . name ) : "" ;
if ( name === "AbortError" ) return true ;
const message =
"message" in err && typeof err . message === "string"
? err . message . toLowerCase ( )
: "" ;
return message . includes ( "aborted" ) ;
} ;
2026-01-03 21:30:40 +00:00
type EmbeddedSandboxInfo = {
enabled : boolean ;
workspaceDir? : string ;
2026-01-07 09:32:49 +00:00
workspaceAccess ? : "none" | "ro" | "rw" ;
agentWorkspaceMount? : string ;
2026-01-03 21:30:40 +00:00
browserControlUrl? : string ;
browserNoVncUrl? : string ;
2026-01-11 01:24:02 +01:00
hostBrowserAllowed? : boolean ;
2026-01-11 01:52:23 +01:00
allowedControlUrls? : string [ ] ;
allowedControlHosts? : string [ ] ;
allowedControlPorts? : number [ ] ;
2026-01-10 21:37:04 +01:00
elevated ? : {
allowed : boolean ;
defaultLevel : "on" | "off" ;
} ;
2026-01-03 21:30:40 +00:00
} ;
2025-12-22 21:02:48 +00:00
2025-12-25 23:50:52 +01:00
function resolveSessionLane ( key : string ) {
const cleaned = key . trim ( ) || "main" ;
return cleaned . startsWith ( "session:" ) ? cleaned : ` session: ${ cleaned } ` ;
}
function resolveGlobalLane ( lane? : string ) {
const cleaned = lane ? . trim ( ) ;
return cleaned ? cleaned : "main" ;
}
2026-01-05 23:02:13 +00:00
function resolveUserTimezone ( configured? : string ) : string {
const trimmed = configured ? . trim ( ) ;
if ( trimmed ) {
try {
new Intl . DateTimeFormat ( "en-US" , { timeZone : trimmed } ) . format (
new Date ( ) ,
) ;
return trimmed ;
} catch {
// ignore invalid timezone
}
}
const host = Intl . DateTimeFormat ( ) . resolvedOptions ( ) . timeZone ;
return host ? . trim ( ) || "UTC" ;
}
function formatUserTime ( date : Date , timeZone : string ) : string | undefined {
try {
const parts = new Intl . DateTimeFormat ( "en-CA" , {
timeZone ,
2026-01-07 11:02:39 +01:00
weekday : "long" ,
2026-01-05 23:02:13 +00:00
year : "numeric" ,
month : "2-digit" ,
day : "2-digit" ,
hour : "2-digit" ,
minute : "2-digit" ,
hourCycle : "h23" ,
} ) . formatToParts ( date ) ;
const map : Record < string , string > = { } ;
for ( const part of parts ) {
if ( part . type !== "literal" ) map [ part . type ] = part . value ;
}
2026-01-07 11:02:39 +01:00
if (
! map . weekday ||
! map . year ||
! map . month ||
! map . day ||
! map . hour ||
! map . minute
) {
2026-01-05 23:02:13 +00:00
return undefined ;
}
2026-01-07 11:02:39 +01:00
return ` ${ map . weekday } ${ map . year } - ${ map . month } - ${ map . day } ${ map . hour } : ${ map . minute } ` ;
2026-01-05 23:02:13 +00:00
} catch {
return undefined ;
}
}
2026-01-05 23:05:57 +00:00
function describeUnknownError ( error : unknown ) : string {
if ( error instanceof Error ) return error . message ;
if ( typeof error === "string" ) return error ;
try {
const serialized = JSON . stringify ( error ) ;
return serialized ? ? "Unknown error" ;
} catch {
return "Unknown error" ;
}
}
2026-01-03 21:30:40 +00:00
export function buildEmbeddedSandboxInfo (
sandbox? : Awaited < ReturnType < typeof resolveSandboxContext > > ,
2026-01-12 02:49:55 +00:00
execElevated? : ExecElevatedDefaults ,
2026-01-03 21:30:40 +00:00
) : EmbeddedSandboxInfo | undefined {
if ( ! sandbox ? . enabled ) return undefined ;
2026-01-10 20:55:50 +00:00
const elevatedAllowed = Boolean (
2026-01-12 02:49:55 +00:00
execElevated ? . enabled && execElevated . allowed ,
2026-01-10 20:55:50 +00:00
) ;
2026-01-03 21:30:40 +00:00
return {
enabled : true ,
workspaceDir : sandbox.workspaceDir ,
2026-01-07 09:32:49 +00:00
workspaceAccess : sandbox.workspaceAccess ,
agentWorkspaceMount :
sandbox . workspaceAccess === "ro" ? "/agent" : undefined ,
2026-01-03 21:30:40 +00:00
browserControlUrl : sandbox.browser?.controlUrl ,
browserNoVncUrl : sandbox.browser?.noVncUrl ,
2026-01-11 01:24:02 +01:00
hostBrowserAllowed : sandbox.browserAllowHostControl ,
2026-01-11 01:52:23 +01:00
allowedControlUrls : sandbox.browserAllowedControlUrls ,
allowedControlHosts : sandbox.browserAllowedControlHosts ,
allowedControlPorts : sandbox.browserAllowedControlPorts ,
2026-01-10 21:37:04 +01:00
. . . ( elevatedAllowed
? {
elevated : {
allowed : true ,
2026-01-12 02:49:55 +00:00
defaultLevel : execElevated?.defaultLevel ? ? "off" ,
2026-01-10 21:37:04 +01:00
} ,
}
: { } ) ,
2026-01-03 21:30:40 +00:00
} ;
}
2026-01-08 02:20:18 +01:00
function buildEmbeddedSystemPrompt ( params : {
2026-01-08 00:08:22 +00:00
workspaceDir : string ;
defaultThinkLevel? : ThinkLevel ;
2026-01-10 22:24:13 +01:00
reasoningLevel? : ReasoningLevel ;
2026-01-08 00:08:22 +00:00
extraSystemPrompt? : string ;
ownerNumbers? : string [ ] ;
reasoningTagHint : boolean ;
heartbeatPrompt? : string ;
2026-01-09 21:20:38 +01:00
skillsPrompt? : string ;
2026-01-08 00:08:22 +00:00
runtimeInfo : {
host : string ;
os : string ;
arch : string ;
node : string ;
model : string ;
2026-01-09 20:46:11 +01:00
provider? : string ;
capabilities? : string [ ] ;
2026-01-08 00:08:22 +00:00
} ;
sandboxInfo? : EmbeddedSandboxInfo ;
tools : AgentTool [ ] ;
modelAliasLines : string [ ] ;
userTimezone : string ;
userTime? : string ;
2026-01-08 02:20:18 +01:00
contextFiles? : EmbeddedContextFile [ ] ;
2026-01-08 00:08:22 +00:00
} ) : string {
2026-01-08 02:20:18 +01:00
return buildAgentSystemPrompt ( {
2026-01-08 00:08:22 +00:00
workspaceDir : params.workspaceDir ,
defaultThinkLevel : params.defaultThinkLevel ,
2026-01-10 22:24:13 +01:00
reasoningLevel : params.reasoningLevel ,
2026-01-08 00:08:22 +00:00
extraSystemPrompt : params.extraSystemPrompt ,
ownerNumbers : params.ownerNumbers ,
reasoningTagHint : params.reasoningTagHint ,
heartbeatPrompt : params.heartbeatPrompt ,
2026-01-09 21:20:38 +01:00
skillsPrompt : params.skillsPrompt ,
2026-01-08 00:08:22 +00:00
runtimeInfo : params.runtimeInfo ,
sandboxInfo : params.sandboxInfo ,
toolNames : params.tools.map ( ( tool ) = > tool . name ) ,
2026-01-11 11:45:25 +00:00
toolSummaries : buildToolSummaryMap ( params . tools ) ,
2026-01-08 00:08:22 +00:00
modelAliasLines : params.modelAliasLines ,
userTimezone : params.userTimezone ,
userTime : params.userTime ,
2026-01-08 02:20:18 +01:00
contextFiles : params.contextFiles ,
2026-01-08 00:08:22 +00:00
} ) ;
}
2026-01-08 02:20:18 +01:00
export function createSystemPromptOverride (
systemPrompt : string ,
2026-01-08 00:01:40 +00:00
) : ( defaultPrompt : string ) = > string {
2026-01-08 02:20:18 +01:00
const trimmed = systemPrompt . trim ( ) ;
return ( ) = > trimmed ;
2026-01-08 00:01:40 +00:00
}
2026-01-10 04:01:00 +01:00
// We always pass tools via `customTools` so our policy filtering, sandbox integration,
// and extended toolset remain consistent across providers.
2026-01-07 06:12:56 +00:00
2026-01-07 21:55:47 +01:00
type AnyAgentTool = AgentTool ;
2026-01-07 06:12:56 +00:00
export function splitSdkTools ( options : {
tools : AnyAgentTool [ ] ;
sandboxEnabled : boolean ;
} ) : {
builtInTools : AnyAgentTool [ ] ;
customTools : ReturnType < typeof toToolDefinitions > ;
} {
2026-01-10 04:01:00 +01:00
// Always pass all tools as customTools so the SDK doesn't "helpfully" swap in
// its own built-in implementations (we need our tool wrappers + policy).
2026-01-08 23:36:33 -05:00
const { tools } = options ;
2026-01-07 06:12:56 +00:00
return {
2026-01-08 23:36:33 -05:00
builtInTools : [ ] ,
customTools : toToolDefinitions ( tools ) ,
2026-01-07 06:12:56 +00:00
} ;
}
2025-12-22 20:45:22 +00:00
export function queueEmbeddedPiMessage (
sessionId : string ,
text : string ,
) : boolean {
const handle = ACTIVE_EMBEDDED_RUNS . get ( sessionId ) ;
if ( ! handle ) return false ;
if ( ! handle . isStreaming ( ) ) return false ;
2026-01-06 05:33:08 +01:00
if ( handle . isCompacting ( ) ) return false ;
2025-12-22 20:45:22 +00:00
void handle . queueMessage ( text ) ;
return true ;
}
2025-12-26 13:35:44 +01:00
export function abortEmbeddedPiRun ( sessionId : string ) : boolean {
const handle = ACTIVE_EMBEDDED_RUNS . get ( sessionId ) ;
if ( ! handle ) return false ;
handle . abort ( ) ;
return true ;
}
export function isEmbeddedPiRunActive ( sessionId : string ) : boolean {
return ACTIVE_EMBEDDED_RUNS . has ( sessionId ) ;
}
export function isEmbeddedPiRunStreaming ( sessionId : string ) : boolean {
const handle = ACTIVE_EMBEDDED_RUNS . get ( sessionId ) ;
if ( ! handle ) return false ;
return handle . isStreaming ( ) ;
}
2026-01-03 23:57:17 +00:00
export function waitForEmbeddedPiRunEnd (
sessionId : string ,
timeoutMs = 15 _000 ,
) : Promise < boolean > {
if ( ! sessionId || ! ACTIVE_EMBEDDED_RUNS . has ( sessionId ) )
return Promise . resolve ( true ) ;
return new Promise ( ( resolve ) = > {
const waiters = EMBEDDED_RUN_WAITERS . get ( sessionId ) ? ? new Set ( ) ;
const waiter : EmbeddedRunWaiter = {
resolve ,
2026-01-04 01:16:53 +01:00
timer : setTimeout (
( ) = > {
waiters . delete ( waiter ) ;
if ( waiters . size === 0 ) EMBEDDED_RUN_WAITERS . delete ( sessionId ) ;
resolve ( false ) ;
} ,
Math . max ( 100 , timeoutMs ) ,
) ,
2026-01-03 23:57:17 +00:00
} ;
waiters . add ( waiter ) ;
EMBEDDED_RUN_WAITERS . set ( sessionId , waiters ) ;
if ( ! ACTIVE_EMBEDDED_RUNS . has ( sessionId ) ) {
waiters . delete ( waiter ) ;
if ( waiters . size === 0 ) EMBEDDED_RUN_WAITERS . delete ( sessionId ) ;
clearTimeout ( waiter . timer ) ;
resolve ( true ) ;
}
} ) ;
}
function notifyEmbeddedRunEnded ( sessionId : string ) {
const waiters = EMBEDDED_RUN_WAITERS . get ( sessionId ) ;
if ( ! waiters || waiters . size === 0 ) return ;
EMBEDDED_RUN_WAITERS . delete ( sessionId ) ;
for ( const waiter of waiters ) {
clearTimeout ( waiter . timer ) ;
waiter . resolve ( true ) ;
}
}
2025-12-26 13:35:44 +01:00
export function resolveEmbeddedSessionLane ( key : string ) {
return resolveSessionLane ( key ) ;
}
2025-12-22 20:45:22 +00:00
function mapThinkingLevel ( level? : ThinkLevel ) : ThinkingLevel {
2026-01-04 14:32:47 +00:00
// pi-agent-core supports "xhigh" too; Clawdbot doesn't surface it for now.
2025-12-22 20:45:22 +00:00
if ( ! level ) return "off" ;
return level ;
}
2026-01-12 02:49:55 +00:00
function resolveExecToolDefaults (
config? : ClawdbotConfig ,
) : ExecToolDefaults | undefined {
const tools = config ? . tools ;
if ( ! tools ) return undefined ;
if ( ! tools . exec ) return tools . bash ;
if ( ! tools . bash ) return tools . exec ;
return { . . . tools . bash , . . . tools . exec } ;
}
2025-12-22 20:45:22 +00:00
function resolveModel (
provider : string ,
modelId : string ,
agentDir? : string ,
2026-01-12 17:13:24 +00:00
cfg? : ClawdbotConfig ,
2025-12-26 11:49:13 +01:00
) : {
model? : Model < Api > ;
error? : string ;
authStorage : ReturnType < typeof discoverAuthStorage > ;
modelRegistry : ReturnType < typeof discoverModels > ;
} {
2026-01-04 14:32:47 +00:00
const resolvedAgentDir = agentDir ? ? resolveClawdbotAgentDir ( ) ;
2025-12-26 10:16:50 +01:00
const authStorage = discoverAuthStorage ( resolvedAgentDir ) ;
const modelRegistry = discoverModels ( authStorage , resolvedAgentDir ) ;
const model = modelRegistry . find ( provider , modelId ) as Model < Api > | null ;
2025-12-26 11:49:13 +01:00
if ( ! model ) {
2026-01-12 17:13:24 +00:00
const providers = cfg ? . models ? . providers ? ? { } ;
const inlineModels =
providers [ provider ] ? . models ? ?
Object . values ( providers )
. flatMap ( ( entry ) = > entry ? . models ? ? [ ] )
. map ( ( entry ) = > ( { . . . entry , provider } ) ) ;
const inlineMatch = inlineModels . find ( ( entry ) = > entry . id === modelId ) ;
if ( inlineMatch ) {
const normalized = normalizeModelCompat ( inlineMatch as Model < Api > ) ;
return {
model : normalized ,
authStorage ,
modelRegistry ,
} ;
}
const providerCfg = providers [ provider ] ;
if ( providerCfg || modelId . startsWith ( "mock-" ) ) {
const fallbackModel : Model < Api > = normalizeModelCompat ( {
id : modelId ,
name : modelId ,
api : providerCfg?.api ? ? "openai-responses" ,
provider ,
reasoning : false ,
input : [ "text" ] ,
cost : { input : 0 , output : 0 , cacheRead : 0 , cacheWrite : 0 } ,
contextWindow :
providerCfg ? . models ? . [ 0 ] ? . contextWindow ? ? DEFAULT_CONTEXT_TOKENS ,
maxTokens :
providerCfg ? . models ? . [ 0 ] ? . maxTokens ? ? DEFAULT_CONTEXT_TOKENS ,
} as Model < Api > ) ;
return { model : fallbackModel , authStorage , modelRegistry } ;
}
2025-12-26 11:49:13 +01:00
return {
error : ` Unknown model: ${ provider } / ${ modelId } ` ,
authStorage ,
modelRegistry ,
} ;
}
2026-01-11 04:28:16 +01:00
return { model : normalizeModelCompat ( model ) , authStorage , modelRegistry } ;
2025-12-22 20:45:22 +00:00
}
2026-01-06 02:06:06 +01:00
export async function compactEmbeddedPiSession ( params : {
sessionId : string ;
sessionKey? : string ;
2026-01-06 18:25:37 +00:00
messageProvider? : string ;
2026-01-08 08:49:16 +01:00
agentAccountId? : string ;
2026-01-06 02:06:06 +01:00
sessionFile : string ;
workspaceDir : string ;
2026-01-06 18:25:37 +00:00
agentDir? : string ;
2026-01-06 02:06:06 +01:00
config? : ClawdbotConfig ;
skillsSnapshot? : SkillSnapshot ;
provider? : string ;
model? : string ;
thinkLevel? : ThinkLevel ;
2026-01-10 22:34:21 +01:00
reasoningLevel? : ReasoningLevel ;
2026-01-12 02:49:55 +00:00
bashElevated? : ExecElevatedDefaults ;
2026-01-06 02:06:06 +01:00
customInstructions? : string ;
lane? : string ;
enqueue? : typeof enqueueCommand ;
extraSystemPrompt? : string ;
ownerNumbers? : string [ ] ;
} ) : Promise < EmbeddedPiCompactResult > {
const sessionLane = resolveSessionLane (
params . sessionKey ? . trim ( ) || params . sessionId ,
) ;
const globalLane = resolveGlobalLane ( params . lane ) ;
const enqueueGlobal =
params . enqueue ? ?
( ( task , opts ) = > enqueueCommandInLane ( globalLane , task , opts ) ) ;
return enqueueCommandInLane ( sessionLane , ( ) = >
enqueueGlobal ( async ( ) = > {
const resolvedWorkspace = resolveUserPath ( params . workspaceDir ) ;
const prevCwd = process . cwd ( ) ;
const provider =
( params . provider ? ? DEFAULT_PROVIDER ) . trim ( ) || DEFAULT_PROVIDER ;
const modelId = ( params . model ? ? DEFAULT_MODEL ) . trim ( ) || DEFAULT_MODEL ;
2026-01-06 18:25:37 +00:00
const agentDir = params . agentDir ? ? resolveClawdbotAgentDir ( ) ;
2026-01-10 18:12:32 +00:00
await ensureClawdbotModelsJson ( params . config , agentDir ) ;
2026-01-06 02:06:06 +01:00
const { model , error , authStorage , modelRegistry } = resolveModel (
provider ,
modelId ,
agentDir ,
2026-01-12 17:13:24 +00:00
params . config ,
2026-01-06 02:06:06 +01:00
) ;
if ( ! model ) {
return {
ok : false ,
compacted : false ,
reason : error ? ? ` Unknown model: ${ provider } / ${ modelId } ` ,
} ;
}
try {
2026-01-06 02:43:35 +01:00
const apiKeyInfo = await getApiKeyForModel ( {
2026-01-06 01:38:09 +00:00
model ,
cfg : params.config ,
} ) ;
2026-01-11 05:19:07 +02:00
if ( model . provider === "github-copilot" ) {
const { resolveCopilotApiToken } = await import (
"../providers/github-copilot-token.js"
) ;
const copilotToken = await resolveCopilotApiToken ( {
githubToken : apiKeyInfo.apiKey ,
} ) ;
authStorage . setRuntimeApiKey ( model . provider , copilotToken . token ) ;
} else {
authStorage . setRuntimeApiKey ( model . provider , apiKeyInfo . apiKey ) ;
}
2026-01-06 02:06:06 +01:00
} catch ( err ) {
return {
ok : false ,
compacted : false ,
reason : describeUnknownError ( err ) ,
} ;
}
await fs . mkdir ( resolvedWorkspace , { recursive : true } ) ;
2026-01-07 09:32:49 +00:00
const sandboxSessionKey = params . sessionKey ? . trim ( ) || params . sessionId ;
const sandbox = await resolveSandboxContext ( {
config : params.config ,
sessionKey : sandboxSessionKey ,
workspaceDir : resolvedWorkspace ,
} ) ;
const effectiveWorkspace = sandbox ? . enabled
? sandbox . workspaceAccess === "rw"
? resolvedWorkspace
: sandbox . workspaceDir
: resolvedWorkspace ;
await fs . mkdir ( effectiveWorkspace , { recursive : true } ) ;
2026-01-06 02:06:06 +01:00
await ensureSessionHeader ( {
sessionFile : params.sessionFile ,
sessionId : params.sessionId ,
2026-01-07 09:32:49 +00:00
cwd : effectiveWorkspace ,
2026-01-06 02:06:06 +01:00
} ) ;
let restoreSkillEnv : ( ( ) = > void ) | undefined ;
2026-01-07 09:32:49 +00:00
process . chdir ( effectiveWorkspace ) ;
2026-01-06 02:06:06 +01:00
try {
const shouldLoadSkillEntries =
! params . skillsSnapshot || ! params . skillsSnapshot . resolvedSkills ;
const skillEntries = shouldLoadSkillEntries
2026-01-07 09:32:49 +00:00
? loadWorkspaceSkillEntries ( effectiveWorkspace )
2026-01-06 02:06:06 +01:00
: [ ] ;
restoreSkillEnv = params . skillsSnapshot
? applySkillEnvOverridesFromSnapshot ( {
snapshot : params.skillsSnapshot ,
config : params.config ,
} )
: applySkillEnvOverrides ( {
skills : skillEntries ? ? [ ] ,
config : params.config ,
} ) ;
2026-01-09 21:27:11 +01:00
const skillsPrompt = resolveSkillsPromptForRun ( {
2026-01-09 21:20:38 +01:00
skillsSnapshot : params.skillsSnapshot ,
2026-01-09 21:27:11 +01:00
entries : shouldLoadSkillEntries ? skillEntries : undefined ,
2026-01-09 21:20:38 +01:00
config : params.config ,
workspaceDir : effectiveWorkspace ,
} ) ;
2026-01-06 02:06:06 +01:00
2026-01-10 00:01:16 +00:00
const bootstrapFiles = filterBootstrapFilesForSession (
await loadWorkspaceBootstrapFiles ( effectiveWorkspace ) ,
params . sessionKey ? ? params . sessionId ,
) ;
2026-01-06 02:06:06 +01:00
const contextFiles = buildBootstrapContextFiles ( bootstrapFiles ) ;
2026-01-10 00:45:10 +00:00
const runAbortController = new AbortController ( ) ;
2026-01-06 02:06:06 +01:00
const tools = createClawdbotCodingTools ( {
2026-01-12 02:49:55 +00:00
exec : {
. . . resolveExecToolDefaults ( params . config ) ,
2026-01-06 02:06:06 +01:00
elevated : params.bashElevated ,
} ,
sandbox ,
2026-01-06 18:25:37 +00:00
messageProvider : params.messageProvider ,
2026-01-08 08:49:16 +01:00
agentAccountId : params.agentAccountId ,
2026-01-06 02:06:06 +01:00
sessionKey : params.sessionKey ? ? params . sessionId ,
2026-01-06 18:25:37 +00:00
agentDir ,
2026-01-10 05:36:09 +00:00
workspaceDir : effectiveWorkspace ,
2026-01-06 02:06:06 +01:00
config : params.config ,
2026-01-10 01:26:20 +01:00
abortSignal : runAbortController.signal ,
2026-01-10 03:05:56 +00:00
modelProvider : model.provider ,
2026-01-12 03:42:49 +00:00
modelId ,
2026-01-10 03:05:56 +00:00
modelAuthMode : resolveModelAuthMode ( model . provider , params . config ) ,
2026-01-08 16:04:52 -08:00
// No currentChannelId/currentThreadTs for compaction - not in message context
2026-01-06 02:06:06 +01:00
} ) ;
const machineName = await getMachineDisplayName ( ) ;
2026-01-09 20:46:11 +01:00
const runtimeProvider = normalizeMessageProvider (
params . messageProvider ,
) ;
const runtimeCapabilities = runtimeProvider
? ( resolveProviderCapabilities ( {
cfg : params.config ,
provider : runtimeProvider ,
accountId : params.agentAccountId ,
} ) ? ? [ ] )
: undefined ;
2026-01-06 02:06:06 +01:00
const runtimeInfo = {
host : machineName ,
os : ` ${ os . type ( ) } ${ os . release ( ) } ` ,
arch : os.arch ( ) ,
node : process.version ,
model : ` ${ provider } / ${ modelId } ` ,
2026-01-09 20:46:11 +01:00
provider : runtimeProvider ,
capabilities : runtimeCapabilities ,
2026-01-06 02:06:06 +01:00
} ;
2026-01-10 21:37:04 +01:00
const sandboxInfo = buildEmbeddedSandboxInfo (
sandbox ,
params . bashElevated ,
) ;
2026-01-12 15:50:32 +13:00
const reasoningTagHint = isReasoningTagProvider ( provider ) ;
2026-01-06 02:06:06 +01:00
const userTimezone = resolveUserTimezone (
2026-01-09 12:44:23 +00:00
params . config ? . agents ? . defaults ? . userTimezone ,
2026-01-06 02:06:06 +01:00
) ;
const userTime = formatUserTime ( new Date ( ) , userTimezone ) ;
2026-01-10 12:52:54 +13:00
// Only include heartbeat prompt for the default agent
2026-01-10 01:30:45 +01:00
const { defaultAgentId , sessionAgentId } = resolveSessionAgentIds ( {
sessionKey : params.sessionKey ,
config : params.config ,
} ) ;
2026-01-10 12:52:54 +13:00
const isDefaultAgent = sessionAgentId === defaultAgentId ;
2026-01-08 02:20:18 +01:00
const appendPrompt = buildEmbeddedSystemPrompt ( {
2026-01-08 00:01:40 +00:00
workspaceDir : effectiveWorkspace ,
defaultThinkLevel : params.thinkLevel ,
2026-01-10 22:24:13 +01:00
reasoningLevel : params.reasoningLevel ? ? "off" ,
2026-01-08 00:01:40 +00:00
extraSystemPrompt : params.extraSystemPrompt ,
ownerNumbers : params.ownerNumbers ,
reasoningTagHint ,
2026-01-10 12:52:54 +13:00
heartbeatPrompt : isDefaultAgent
? resolveHeartbeatPrompt (
params . config ? . agents ? . defaults ? . heartbeat ? . prompt ,
)
: undefined ,
2026-01-09 21:20:38 +01:00
skillsPrompt ,
2026-01-08 00:01:40 +00:00
runtimeInfo ,
sandboxInfo ,
2026-01-08 00:08:22 +00:00
tools ,
2026-01-08 00:01:40 +00:00
modelAliasLines : buildModelAliasLines ( params . config ) ,
userTimezone ,
userTime ,
2026-01-08 02:20:18 +01:00
contextFiles ,
2026-01-06 02:06:06 +01:00
} ) ;
2026-01-08 02:20:18 +01:00
const systemPrompt = createSystemPromptOverride ( appendPrompt ) ;
2026-01-06 02:06:06 +01:00
2026-01-11 02:24:25 +00:00
const sessionLock = await acquireSessionWriteLock ( {
sessionFile : params.sessionFile ,
2026-01-07 06:12:56 +00:00
} ) ;
2026-01-06 02:06:06 +01:00
try {
2026-01-11 02:24:25 +00:00
// Pre-warm session file to bring it into OS page cache
await prewarmSessionFile ( params . sessionFile ) ;
2026-01-12 17:28:39 +00:00
const sessionManager = guardSessionManager (
SessionManager . open ( params . sessionFile ) ,
) ;
2026-01-11 02:24:25 +00:00
trackSessionManagerAccess ( params . sessionFile ) ;
const settingsManager = SettingsManager . create (
effectiveWorkspace ,
agentDir ,
) ;
2026-01-12 05:28:17 +00:00
ensurePiCompactionReserveTokens ( {
settingsManager ,
minReserveTokens : resolveCompactionReserveTokensFloor (
params . config ,
) ,
} ) ;
2026-01-11 04:46:18 +00:00
const additionalExtensionPaths = buildEmbeddedExtensionPaths ( {
2026-01-11 02:24:25 +00:00
cfg : params.config ,
2026-01-07 22:04:53 +01:00
sessionManager ,
2026-01-11 02:24:25 +00:00
provider ,
modelId ,
model ,
2026-01-07 22:04:53 +01:00
} ) ;
2026-01-11 02:24:25 +00:00
const { builtInTools , customTools } = splitSdkTools ( {
tools ,
sandboxEnabled : ! ! sandbox ? . enabled ,
} ) ;
let session : Awaited <
ReturnType < typeof createAgentSession >
> [ "session" ] ;
( { session } = await createAgentSession ( {
cwd : resolvedWorkspace ,
agentDir ,
authStorage ,
modelRegistry ,
model ,
thinkingLevel : mapThinkingLevel ( params . thinkLevel ) ,
systemPrompt ,
tools : builtInTools ,
customTools ,
sessionManager ,
settingsManager ,
skills : [ ] ,
contextFiles : [ ] ,
additionalExtensionPaths ,
} ) ) ;
2026-01-11 23:55:14 +00:00
// Wire up config-driven model params (e.g., temperature/maxTokens)
2026-01-11 15:56:43 +00:00
applyExtraParamsToAgent (
session . agent ,
params . config ,
provider ,
modelId ,
params . thinkLevel ,
) ;
2026-01-11 02:24:25 +00:00
try {
const prior = await sanitizeSessionHistory ( {
messages : session.messages ,
modelApi : model.api ,
sessionManager ,
sessionId : params.sessionId ,
} ) ;
2026-01-12 16:41:10 -05:00
// Validate turn ordering for both Gemini (consecutive assistant) and Anthropic (consecutive user)
const validatedGemini = validateGeminiTurns ( prior ) ;
const validated = validateAnthropicTurns ( validatedGemini ) ;
2026-01-11 08:21:14 -06:00
const limited = limitHistoryTurns (
validated ,
2026-01-11 08:38:19 -06:00
getDmHistoryLimitFromSessionKey ( params . sessionKey , params . config ) ,
2026-01-11 08:21:14 -06:00
) ;
if ( limited . length > 0 ) {
session . agent . replaceMessages ( limited ) ;
2026-01-11 02:24:25 +00:00
}
const result = await session . compact ( params . customInstructions ) ;
return {
ok : true ,
compacted : true ,
result : {
summary : result.summary ,
firstKeptEntryId : result.firstKeptEntryId ,
tokensBefore : result.tokensBefore ,
details : result.details ,
} ,
} ;
} finally {
2026-01-12 17:28:39 +00:00
sessionManager . flushPendingToolResults ? . ( ) ;
2026-01-11 02:24:25 +00:00
session . dispose ( ) ;
2026-01-06 02:06:06 +01:00
}
} finally {
2026-01-11 02:24:25 +00:00
await sessionLock . release ( ) ;
2026-01-06 02:06:06 +01:00
}
} catch ( err ) {
return {
ok : false ,
compacted : false ,
reason : describeUnknownError ( err ) ,
} ;
} finally {
restoreSkillEnv ? . ( ) ;
process . chdir ( prevCwd ) ;
}
} ) ,
) ;
}
2025-12-22 20:45:22 +00:00
export async function runEmbeddedPiAgent ( params : {
sessionId : string ;
2025-12-25 23:50:52 +01:00
sessionKey? : string ;
2026-01-06 18:25:37 +00:00
messageProvider? : string ;
2026-01-08 08:49:16 +01:00
agentAccountId? : string ;
2026-01-08 16:04:52 -08:00
/** Current channel ID for auto-threading (Slack). */
currentChannelId? : string ;
/** Current thread timestamp for auto-threading (Slack). */
currentThreadTs? : string ;
/** Reply-to mode for Slack auto-threading. */
replyToMode ? : "off" | "first" | "all" ;
/** Mutable ref to track if a reply was sent (for "first" mode). */
hasRepliedRef ? : { value : boolean } ;
2025-12-22 20:45:22 +00:00
sessionFile : string ;
workspaceDir : string ;
2026-01-06 18:25:37 +00:00
agentDir? : string ;
2026-01-04 14:32:47 +00:00
config? : ClawdbotConfig ;
2025-12-22 20:45:22 +00:00
skillsSnapshot? : SkillSnapshot ;
prompt : string ;
2026-01-10 19:17:32 +02:00
/** Optional image attachments for multimodal messages. */
images? : ImageContent [ ] ;
2025-12-22 20:45:22 +00:00
provider? : string ;
model? : string ;
2026-01-06 00:56:29 +00:00
authProfileId? : string ;
2025-12-22 20:45:22 +00:00
thinkLevel? : ThinkLevel ;
verboseLevel? : VerboseLevel ;
2026-01-07 06:16:38 +01:00
reasoningLevel? : ReasoningLevel ;
2026-01-12 02:49:55 +00:00
bashElevated? : ExecElevatedDefaults ;
2025-12-22 20:45:22 +00:00
timeoutMs : number ;
runId : string ;
abortSignal? : AbortSignal ;
shouldEmitToolResult ? : ( ) = > boolean ;
onPartialReply ? : ( payload : {
text? : string ;
mediaUrls? : string [ ] ;
} ) = > void | Promise < void > ;
2026-01-03 00:28:33 +01:00
onBlockReply ? : ( payload : {
text? : string ;
mediaUrls? : string [ ] ;
2026-01-08 12:40:31 +00:00
audioAsVoice? : boolean ;
2026-01-03 00:28:33 +01:00
} ) = > void | Promise < void > ;
2026-01-11 21:19:50 -05:00
/** Flush pending block replies (e.g., before tool execution to preserve message boundaries). */
onBlockReplyFlush ? : ( ) = > void | Promise < void > ;
2026-01-03 00:52:02 +01:00
blockReplyBreak ? : "text_end" | "message_end" ;
2026-01-03 16:45:53 +01:00
blockReplyChunking? : BlockReplyChunking ;
2026-01-07 11:08:11 +01:00
onReasoningStream ? : ( payload : {
text? : string ;
mediaUrls? : string [ ] ;
} ) = > void | Promise < void > ;
2025-12-22 20:45:22 +00:00
onToolResult ? : ( payload : {
text? : string ;
mediaUrls? : string [ ] ;
} ) = > void | Promise < void > ;
onAgentEvent ? : ( evt : {
stream : string ;
data : Record < string , unknown > ;
} ) = > void ;
2025-12-25 23:50:52 +01:00
lane? : string ;
2025-12-22 20:45:22 +00:00
enqueue? : typeof enqueueCommand ;
2025-12-23 13:32:07 +00:00
extraSystemPrompt? : string ;
2025-12-23 14:19:41 +00:00
ownerNumbers? : string [ ] ;
2025-12-24 00:52:33 +00:00
enforceFinalTag? : boolean ;
2025-12-22 20:45:22 +00:00
} ) : Promise < EmbeddedPiRunResult > {
2026-01-06 23:05:05 +00:00
const sessionLane = resolveSessionLane (
params . sessionKey ? . trim ( ) || params . sessionId ,
) ;
2025-12-25 23:50:52 +01:00
const globalLane = resolveGlobalLane ( params . lane ) ;
const enqueueGlobal =
params . enqueue ? ?
( ( task , opts ) = > enqueueCommandInLane ( globalLane , task , opts ) ) ;
2026-01-10 01:26:20 +01:00
const runAbortController = new AbortController ( ) ;
2025-12-25 23:50:52 +01:00
return enqueueCommandInLane ( sessionLane , ( ) = >
enqueueGlobal ( async ( ) = > {
const started = Date . now ( ) ;
2026-01-03 20:16:53 +00:00
const resolvedWorkspace = resolveUserPath ( params . workspaceDir ) ;
2025-12-25 23:50:52 +01:00
const prevCwd = process . cwd ( ) ;
const provider =
( params . provider ? ? DEFAULT_PROVIDER ) . trim ( ) || DEFAULT_PROVIDER ;
const modelId = ( params . model ? ? DEFAULT_MODEL ) . trim ( ) || DEFAULT_MODEL ;
2026-01-06 18:25:37 +00:00
const agentDir = params . agentDir ? ? resolveClawdbotAgentDir ( ) ;
2026-01-10 18:12:32 +00:00
await ensureClawdbotModelsJson ( params . config , agentDir ) ;
2025-12-26 11:49:13 +01:00
const { model , error , authStorage , modelRegistry } = resolveModel (
provider ,
modelId ,
agentDir ,
2026-01-12 17:13:24 +00:00
params . config ,
2025-12-26 11:49:13 +01:00
) ;
2025-12-25 23:50:52 +01:00
if ( ! model ) {
throw new Error ( error ? ? ` Unknown model: ${ provider } / ${ modelId } ` ) ;
}
2026-01-10 00:47:34 +01:00
const ctxInfo = resolveContextWindowInfo ( {
cfg : params.config ,
provider ,
modelId ,
modelContextWindow : model.contextWindow ,
defaultTokens : DEFAULT_CONTEXT_TOKENS ,
} ) ;
const ctxGuard = evaluateContextWindowGuard ( {
info : ctxInfo ,
warnBelowTokens : CONTEXT_WINDOW_WARN_BELOW_TOKENS ,
hardMinTokens : CONTEXT_WINDOW_HARD_MIN_TOKENS ,
} ) ;
if ( ctxGuard . shouldWarn ) {
log . warn (
` low context window: ${ provider } / ${ modelId } ctx= ${ ctxGuard . tokens } (warn< ${ CONTEXT_WINDOW_WARN_BELOW_TOKENS } ) source= ${ ctxGuard . source } ` ,
) ;
}
if ( ctxGuard . shouldBlock ) {
log . error (
` blocked model (context window too small): ${ provider } / ${ modelId } ctx= ${ ctxGuard . tokens } (min= ${ CONTEXT_WINDOW_HARD_MIN_TOKENS } ) source= ${ ctxGuard . source } ` ,
) ;
throw new FailoverError (
` Model context window too small ( ${ ctxGuard . tokens } tokens). Minimum is ${ CONTEXT_WINDOW_HARD_MIN_TOKENS } . ` ,
{ reason : "unknown" , provider , model : modelId } ,
) ;
}
2026-01-06 18:25:37 +00:00
const authStore = ensureAuthProfileStore ( agentDir ) ;
2026-01-06 00:56:29 +00:00
const explicitProfileId = params . authProfileId ? . trim ( ) ;
const profileOrder = resolveAuthProfileOrder ( {
cfg : params.config ,
store : authStore ,
provider ,
preferredProfile : explicitProfileId ,
} ) ;
if ( explicitProfileId && ! profileOrder . includes ( explicitProfileId ) ) {
throw new Error (
` Auth profile " ${ explicitProfileId } " is not configured for ${ provider } . ` ,
) ;
}
const profileCandidates =
profileOrder . length > 0 ? profileOrder : [ undefined ] ;
let profileIndex = 0 ;
const initialThinkLevel = params . thinkLevel ? ? "off" ;
let thinkLevel = initialThinkLevel ;
2026-01-05 18:54:23 +01:00
const attemptedThinking = new Set < ThinkLevel > ( ) ;
2026-01-06 01:08:36 +00:00
let apiKeyInfo : ApiKeyInfo | null = null ;
let lastProfileId : string | undefined ;
2026-01-06 00:56:29 +00:00
const resolveApiKeyForCandidate = async ( candidate? : string ) = > {
return getApiKeyForModel ( {
model ,
cfg : params.config ,
profileId : candidate ,
store : authStore ,
} ) ;
} ;
const applyApiKeyInfo = async ( candidate? : string ) : Promise < void > = > {
apiKeyInfo = await resolveApiKeyForCandidate ( candidate ) ;
2026-01-11 05:19:07 +02:00
if ( model . provider === "github-copilot" ) {
const { resolveCopilotApiToken } = await import (
"../providers/github-copilot-token.js"
) ;
const copilotToken = await resolveCopilotApiToken ( {
githubToken : apiKeyInfo.apiKey ,
} ) ;
authStorage . setRuntimeApiKey ( model . provider , copilotToken . token ) ;
} else {
authStorage . setRuntimeApiKey ( model . provider , apiKeyInfo . apiKey ) ;
}
2026-01-06 01:08:36 +00:00
lastProfileId = apiKeyInfo . profileId ;
2026-01-06 00:56:29 +00:00
} ;
const advanceAuthProfile = async ( ) : Promise < boolean > = > {
let nextIndex = profileIndex + 1 ;
while ( nextIndex < profileCandidates . length ) {
const candidate = profileCandidates [ nextIndex ] ;
try {
await applyApiKeyInfo ( candidate ) ;
profileIndex = nextIndex ;
thinkLevel = initialThinkLevel ;
attemptedThinking . clear ( ) ;
return true ;
} catch ( err ) {
if ( candidate && candidate === explicitProfileId ) throw err ;
nextIndex += 1 ;
}
}
return false ;
} ;
try {
await applyApiKeyInfo ( profileCandidates [ profileIndex ] ) ;
} catch ( err ) {
if ( profileCandidates [ profileIndex ] === explicitProfileId ) throw err ;
const advanced = await advanceAuthProfile ( ) ;
if ( ! advanced ) throw err ;
}
2025-12-25 23:50:52 +01:00
2026-01-05 18:54:23 +01:00
while ( true ) {
const thinkingLevel = mapThinkingLevel ( thinkLevel ) ;
attemptedThinking . add ( thinkLevel ) ;
2026-01-03 14:09:19 +00:00
2026-01-05 18:54:23 +01:00
log . debug (
2026-01-06 18:25:37 +00:00
` embedded run start: runId= ${ params . runId } sessionId= ${ params . sessionId } provider= ${ provider } model= ${ modelId } thinking= ${ thinkLevel } messageProvider= ${ params . messageProvider ? ? "unknown" } ` ,
2026-01-05 18:54:23 +01:00
) ;
2025-12-22 20:45:22 +00:00
2026-01-05 18:54:23 +01:00
await fs . mkdir ( resolvedWorkspace , { recursive : true } ) ;
2026-01-07 09:32:49 +00:00
const sandboxSessionKey = params . sessionKey ? . trim ( ) || params . sessionId ;
const sandbox = await resolveSandboxContext ( {
config : params.config ,
sessionKey : sandboxSessionKey ,
workspaceDir : resolvedWorkspace ,
} ) ;
const effectiveWorkspace = sandbox ? . enabled
? sandbox . workspaceAccess === "rw"
? resolvedWorkspace
: sandbox . workspaceDir
: resolvedWorkspace ;
await fs . mkdir ( effectiveWorkspace , { recursive : true } ) ;
2026-01-05 18:54:23 +01:00
let restoreSkillEnv : ( ( ) = > void ) | undefined ;
2026-01-07 09:32:49 +00:00
process . chdir ( effectiveWorkspace ) ;
2026-01-05 18:54:23 +01:00
try {
2026-01-05 22:52:13 +00:00
const shouldLoadSkillEntries =
! params . skillsSnapshot || ! params . skillsSnapshot . resolvedSkills ;
const skillEntries = shouldLoadSkillEntries
2026-01-07 09:32:49 +00:00
? loadWorkspaceSkillEntries ( effectiveWorkspace )
2026-01-05 22:52:13 +00:00
: [ ] ;
restoreSkillEnv = params . skillsSnapshot
? applySkillEnvOverridesFromSnapshot ( {
snapshot : params.skillsSnapshot ,
config : params.config ,
} )
: applySkillEnvOverrides ( {
skills : skillEntries ? ? [ ] ,
config : params.config ,
} ) ;
2026-01-09 21:27:11 +01:00
const skillsPrompt = resolveSkillsPromptForRun ( {
2026-01-09 21:20:38 +01:00
skillsSnapshot : params.skillsSnapshot ,
2026-01-09 21:27:11 +01:00
entries : shouldLoadSkillEntries ? skillEntries : undefined ,
2026-01-09 21:20:38 +01:00
config : params.config ,
workspaceDir : effectiveWorkspace ,
} ) ;
2026-01-05 22:52:13 +00:00
2026-01-10 00:01:16 +00:00
const bootstrapFiles = filterBootstrapFilesForSession (
await loadWorkspaceBootstrapFiles ( effectiveWorkspace ) ,
params . sessionKey ? ? params . sessionId ,
) ;
2026-01-05 22:52:13 +00:00
const contextFiles = buildBootstrapContextFiles ( bootstrapFiles ) ;
// Tool schemas must be provider-compatible (OpenAI requires top-level `type: "object"`).
// `createClawdbotCodingTools()` normalizes schemas so the session can pass them through unchanged.
const tools = createClawdbotCodingTools ( {
2026-01-12 02:49:55 +00:00
exec : {
. . . resolveExecToolDefaults ( params . config ) ,
2026-01-05 22:52:13 +00:00
elevated : params.bashElevated ,
} ,
sandbox ,
2026-01-06 18:25:37 +00:00
messageProvider : params.messageProvider ,
2026-01-08 08:49:16 +01:00
agentAccountId : params.agentAccountId ,
2026-01-05 22:52:13 +00:00
sessionKey : params.sessionKey ? ? params . sessionId ,
2026-01-06 18:25:37 +00:00
agentDir ,
2026-01-10 05:36:09 +00:00
workspaceDir : effectiveWorkspace ,
2026-01-05 22:52:13 +00:00
config : params.config ,
2026-01-10 01:26:20 +01:00
abortSignal : runAbortController.signal ,
2026-01-10 03:05:56 +00:00
modelProvider : model.provider ,
2026-01-12 03:42:49 +00:00
modelId ,
2026-01-10 03:05:56 +00:00
modelAuthMode : resolveModelAuthMode ( model . provider , params . config ) ,
2026-01-08 16:04:52 -08:00
currentChannelId : params.currentChannelId ,
currentThreadTs : params.currentThreadTs ,
replyToMode : params.replyToMode ,
hasRepliedRef : params.hasRepliedRef ,
2026-01-05 22:52:13 +00:00
} ) ;
const machineName = await getMachineDisplayName ( ) ;
const runtimeInfo = {
host : machineName ,
os : ` ${ os . type ( ) } ${ os . release ( ) } ` ,
arch : os.arch ( ) ,
node : process.version ,
model : ` ${ provider } / ${ modelId } ` ,
} ;
2026-01-10 21:37:04 +01:00
const sandboxInfo = buildEmbeddedSandboxInfo (
sandbox ,
params . bashElevated ,
) ;
2026-01-12 15:50:32 +13:00
const reasoningTagHint = isReasoningTagProvider ( provider ) ;
2026-01-05 23:02:13 +00:00
const userTimezone = resolveUserTimezone (
2026-01-09 12:44:23 +00:00
params . config ? . agents ? . defaults ? . userTimezone ,
2026-01-05 23:02:13 +00:00
) ;
const userTime = formatUserTime ( new Date ( ) , userTimezone ) ;
2026-01-10 12:52:54 +13:00
// Only include heartbeat prompt for the default agent
2026-01-10 01:30:45 +01:00
const { defaultAgentId , sessionAgentId } = resolveSessionAgentIds ( {
sessionKey : params.sessionKey ,
config : params.config ,
} ) ;
2026-01-10 12:52:54 +13:00
const isDefaultAgent = sessionAgentId === defaultAgentId ;
2026-01-08 02:20:18 +01:00
const appendPrompt = buildEmbeddedSystemPrompt ( {
2026-01-08 00:01:40 +00:00
workspaceDir : effectiveWorkspace ,
defaultThinkLevel : thinkLevel ,
2026-01-10 22:24:13 +01:00
reasoningLevel : params.reasoningLevel ? ? "off" ,
2026-01-08 00:01:40 +00:00
extraSystemPrompt : params.extraSystemPrompt ,
ownerNumbers : params.ownerNumbers ,
reasoningTagHint ,
2026-01-10 12:52:54 +13:00
heartbeatPrompt : isDefaultAgent
? resolveHeartbeatPrompt (
params . config ? . agents ? . defaults ? . heartbeat ? . prompt ,
)
: undefined ,
2026-01-09 21:20:38 +01:00
skillsPrompt ,
2026-01-08 00:01:40 +00:00
runtimeInfo ,
sandboxInfo ,
2026-01-08 00:08:22 +00:00
tools ,
2026-01-08 00:01:40 +00:00
modelAliasLines : buildModelAliasLines ( params . config ) ,
userTimezone ,
userTime ,
2026-01-08 02:20:18 +01:00
contextFiles ,
2026-01-05 22:52:13 +00:00
} ) ;
2026-01-08 02:20:18 +01:00
const systemPrompt = createSystemPromptOverride ( appendPrompt ) ;
2025-12-22 20:45:22 +00:00
2026-01-11 02:24:25 +00:00
const sessionLock = await acquireSessionWriteLock ( {
sessionFile : params.sessionFile ,
} ) ;
2026-01-07 22:10:39 +08:00
// Pre-warm session file to bring it into OS page cache
await prewarmSessionFile ( params . sessionFile ) ;
2026-01-12 17:45:19 +00:00
const sessionManager = guardSessionManager (
SessionManager . open ( params . sessionFile ) ,
) ;
2026-01-07 22:10:39 +08:00
trackSessionManagerAccess ( params . sessionFile ) ;
2026-01-05 22:52:13 +00:00
const settingsManager = SettingsManager . create (
2026-01-07 09:32:49 +00:00
effectiveWorkspace ,
2026-01-05 22:52:13 +00:00
agentDir ,
) ;
2026-01-12 05:28:17 +00:00
ensurePiCompactionReserveTokens ( {
settingsManager ,
minReserveTokens : resolveCompactionReserveTokensFloor (
params . config ,
) ,
} ) ;
2026-01-11 04:46:18 +00:00
const additionalExtensionPaths = buildEmbeddedExtensionPaths ( {
2026-01-07 12:02:46 +01:00
cfg : params.config ,
sessionManager ,
provider ,
modelId ,
model ,
} ) ;
2025-12-22 20:45:22 +00:00
2026-01-07 06:12:56 +00:00
const { builtInTools , customTools } = splitSdkTools ( {
tools ,
sandboxEnabled : ! ! sandbox ? . enabled ,
} ) ;
2026-01-05 16:18:27 +00:00
2026-01-07 12:02:46 +01:00
let session : Awaited <
ReturnType < typeof createAgentSession >
> [ "session" ] ;
( { session } = await createAgentSession ( {
2026-01-05 22:52:13 +00:00
cwd : resolvedWorkspace ,
agentDir ,
authStorage ,
modelRegistry ,
model ,
thinkingLevel ,
systemPrompt ,
// Built-in tools recognized by pi-coding-agent SDK
tools : builtInTools ,
// Custom clawdbot tools (browser, canvas, nodes, cron, etc.)
customTools ,
sessionManager ,
settingsManager ,
2026-01-08 02:20:18 +01:00
skills : [ ] ,
contextFiles : [ ] ,
2026-01-07 12:02:46 +01:00
additionalExtensionPaths ,
} ) ) ;
2025-12-25 23:50:52 +01:00
2026-01-11 23:55:14 +00:00
// Wire up config-driven model params (e.g., temperature/maxTokens)
2026-01-11 15:56:43 +00:00
applyExtraParamsToAgent (
session . agent ,
params . config ,
provider ,
modelId ,
params . thinkLevel ,
) ;
2026-01-07 12:02:46 +01:00
try {
2026-01-07 22:04:53 +01:00
const prior = await sanitizeSessionHistory ( {
messages : session.messages ,
modelApi : model.api ,
sessionManager ,
sessionId : params.sessionId ,
} ) ;
2026-01-12 16:41:10 -05:00
// Validate turn ordering for both Gemini (consecutive assistant) and Anthropic (consecutive user)
const validatedGemini = validateGeminiTurns ( prior ) ;
const validated = validateAnthropicTurns ( validatedGemini ) ;
2026-01-11 08:21:14 -06:00
const limited = limitHistoryTurns (
validated ,
2026-01-11 08:38:19 -06:00
getDmHistoryLimitFromSessionKey ( params . sessionKey , params . config ) ,
2026-01-11 08:21:14 -06:00
) ;
if ( limited . length > 0 ) {
session . agent . replaceMessages ( limited ) ;
2026-01-07 12:02:46 +01:00
}
} catch ( err ) {
2026-01-12 17:45:19 +00:00
sessionManager . flushPendingToolResults ? . ( ) ;
2026-01-07 12:02:46 +01:00
session . dispose ( ) ;
2026-01-11 02:24:25 +00:00
await sessionLock . release ( ) ;
2026-01-07 12:02:46 +01:00
throw err ;
2026-01-05 22:52:13 +00:00
}
let aborted = Boolean ( params . abortSignal ? . aborted ) ;
2026-01-06 04:48:34 +00:00
let timedOut = false ;
const abortRun = ( isTimeout = false ) = > {
2026-01-05 22:52:13 +00:00
aborted = true ;
2026-01-06 04:48:34 +00:00
if ( isTimeout ) timedOut = true ;
2026-01-10 01:26:20 +01:00
runAbortController . abort ( ) ;
2026-01-05 22:52:13 +00:00
void session . abort ( ) ;
} ;
2026-01-07 12:02:46 +01:00
let subscription : ReturnType < typeof subscribeEmbeddedPiSession > ;
try {
subscription = subscribeEmbeddedPiSession ( {
session ,
runId : params.runId ,
verboseLevel : params.verboseLevel ,
reasoningMode : params.reasoningLevel ? ? "off" ,
shouldEmitToolResult : params.shouldEmitToolResult ,
onToolResult : params.onToolResult ,
onReasoningStream : params.onReasoningStream ,
onBlockReply : params.onBlockReply ,
2026-01-11 21:19:50 -05:00
onBlockReplyFlush : params.onBlockReplyFlush ,
2026-01-07 12:02:46 +01:00
blockReplyBreak : params.blockReplyBreak ,
blockReplyChunking : params.blockReplyChunking ,
onPartialReply : params.onPartialReply ,
onAgentEvent : params.onAgentEvent ,
enforceFinalTag : params.enforceFinalTag ,
} ) ;
} catch ( err ) {
2026-01-12 17:45:19 +00:00
sessionManager . flushPendingToolResults ? . ( ) ;
2026-01-07 12:02:46 +01:00
session . dispose ( ) ;
2026-01-11 02:24:25 +00:00
await sessionLock . release ( ) ;
2026-01-07 12:02:46 +01:00
throw err ;
}
2026-01-06 05:33:08 +01:00
const {
assistantTexts ,
toolMetas ,
unsubscribe ,
waitForCompactionRetry ,
2026-01-08 00:50:29 +00:00
getMessagingToolSentTexts ,
2026-01-08 08:49:16 +01:00
getMessagingToolSentTargets ,
2026-01-07 03:24:56 -03:00
didSendViaMessagingTool ,
2026-01-06 05:33:08 +01:00
} = subscription ;
const queueHandle : EmbeddedPiQueueHandle = {
queueMessage : async ( text : string ) = > {
await session . steer ( text ) ;
} ,
isStreaming : ( ) = > session . isStreaming ,
isCompacting : ( ) = > subscription . isCompacting ( ) ,
abort : abortRun ,
} ;
2026-01-06 23:05:05 +00:00
ACTIVE_EMBEDDED_RUNS . set ( params . sessionId , queueHandle ) ;
2025-12-22 20:45:22 +00:00
2026-01-05 22:52:13 +00:00
let abortWarnTimer : NodeJS.Timeout | undefined ;
const abortTimer = setTimeout (
( ) = > {
log . warn (
` embedded run timeout: runId= ${ params . runId } sessionId= ${ params . sessionId } timeoutMs= ${ params . timeoutMs } ` ,
) ;
2026-01-06 04:48:34 +00:00
abortRun ( true ) ;
2026-01-05 22:52:13 +00:00
if ( ! abortWarnTimer ) {
abortWarnTimer = setTimeout ( ( ) = > {
if ( ! session . isStreaming ) return ;
log . warn (
` embedded run abort still streaming: runId= ${ params . runId } sessionId= ${ params . sessionId } ` ,
) ;
} , 10 _000 ) ;
}
} ,
Math . max ( 1 , params . timeoutMs ) ,
) ;
let messagesSnapshot : AgentMessage [ ] = [ ] ;
let sessionIdUsed = session . sessionId ;
const onAbort = ( ) = > {
2026-01-03 14:57:49 +00:00
abortRun ( ) ;
2026-01-05 22:52:13 +00:00
} ;
if ( params . abortSignal ) {
if ( params . abortSignal . aborted ) {
onAbort ( ) ;
} else {
params . abortSignal . addEventListener ( "abort" , onAbort , {
once : true ,
} ) ;
2026-01-03 14:57:49 +00:00
}
2025-12-25 23:50:52 +01:00
}
2026-01-05 22:52:13 +00:00
let promptError : unknown = null ;
2025-12-25 23:50:52 +01:00
try {
2026-01-05 22:52:13 +00:00
const promptStartedAt = Date . now ( ) ;
2026-01-03 20:57:32 +00:00
log . debug (
2026-01-05 22:52:13 +00:00
` embedded run prompt start: runId= ${ params . runId } sessionId= ${ params . sessionId } ` ,
2026-01-03 14:09:19 +00:00
) ;
2026-01-05 22:52:13 +00:00
try {
2026-01-10 19:17:32 +02:00
await session . prompt ( params . prompt , {
images : params.images ,
} ) ;
2026-01-05 22:52:13 +00:00
} catch ( err ) {
promptError = err ;
} finally {
log . debug (
` embedded run prompt end: runId= ${ params . runId } sessionId= ${ params . sessionId } durationMs= ${ Date . now ( ) - promptStartedAt } ` ,
) ;
}
2026-01-06 22:44:19 +00:00
try {
await waitForCompactionRetry ( ) ;
} catch ( err ) {
2026-01-06 19:34:54 -03:00
// Capture AbortError from waitForCompactionRetry to enable fallback/rotation.
2026-01-07 01:00:47 +01:00
if ( isAbortError ( err ) ) {
if ( ! promptError ) promptError = err ;
} else {
throw err ;
}
2026-01-06 22:44:19 +00:00
}
2026-01-05 22:52:13 +00:00
messagesSnapshot = session . messages . slice ( ) ;
sessionIdUsed = session . sessionId ;
} finally {
clearTimeout ( abortTimer ) ;
if ( abortWarnTimer ) {
clearTimeout ( abortWarnTimer ) ;
abortWarnTimer = undefined ;
}
unsubscribe ( ) ;
2026-01-06 23:05:05 +00:00
if ( ACTIVE_EMBEDDED_RUNS . get ( params . sessionId ) === queueHandle ) {
ACTIVE_EMBEDDED_RUNS . delete ( params . sessionId ) ;
notifyEmbeddedRunEnded ( params . sessionId ) ;
2026-01-05 22:52:13 +00:00
}
2026-01-12 17:45:19 +00:00
sessionManager . flushPendingToolResults ? . ( ) ;
2026-01-05 22:52:13 +00:00
session . dispose ( ) ;
2026-01-11 02:24:25 +00:00
await sessionLock . release ( ) ;
2026-01-05 22:52:13 +00:00
params . abortSignal ? . removeEventListener ? . ( "abort" , onAbort ) ;
2025-12-25 23:50:52 +01:00
}
2026-01-05 22:52:13 +00:00
if ( promptError && ! aborted ) {
2026-01-06 00:56:29 +00:00
const errorText = describeUnknownError ( promptError ) ;
2026-01-07 07:51:04 -06:00
if ( isContextOverflowError ( errorText ) ) {
return {
payloads : [
{
text :
"Context overflow: the conversation history is too large for the model. " +
"Use /new or /reset to start a fresh session, or try a model with a larger context window." ,
isError : true ,
} ,
] ,
meta : {
durationMs : Date.now ( ) - started ,
agentMeta : {
sessionId : sessionIdUsed ,
provider ,
model : model.id ,
} ,
} ,
} ;
}
2026-01-09 21:31:13 +01:00
const promptFailoverReason = classifyFailoverReason ( errorText ) ;
2026-01-06 00:56:29 +00:00
if (
2026-01-09 21:31:13 +01:00
promptFailoverReason &&
promptFailoverReason !== "timeout" &&
lastProfileId
) {
await markAuthProfileFailure ( {
store : authStore ,
profileId : lastProfileId ,
reason : promptFailoverReason ,
2026-01-09 21:57:52 +01:00
cfg : params.config ,
2026-01-09 21:31:13 +01:00
agentDir : params.agentDir ,
} ) ;
}
if (
isFailoverErrorMessage ( errorText ) &&
promptFailoverReason !== "timeout" &&
2026-01-06 00:56:29 +00:00
( await advanceAuthProfile ( ) )
) {
continue ;
}
2026-01-05 22:52:13 +00:00
const fallbackThinking = pickFallbackThinkingLevel ( {
2026-01-06 00:56:29 +00:00
message : errorText ,
2026-01-05 22:52:13 +00:00
attempted : attemptedThinking ,
} ) ;
if ( fallbackThinking ) {
log . warn (
` unsupported thinking level for ${ provider } / ${ modelId } ; retrying with ${ fallbackThinking } ` ,
) ;
thinkLevel = fallbackThinking ;
continue ;
}
throw promptError ;
2025-12-25 23:50:52 +01:00
}
2026-01-05 22:52:13 +00:00
const lastAssistant = messagesSnapshot
. slice ( )
. reverse ( )
. find ( ( m ) = > ( m as AgentMessage ) ? . role === "assistant" ) as
| AssistantMessage
| undefined ;
2026-01-05 18:54:23 +01:00
const fallbackThinking = pickFallbackThinkingLevel ( {
2026-01-05 22:52:13 +00:00
message : lastAssistant?.errorMessage ,
2026-01-05 18:54:23 +01:00
attempted : attemptedThinking ,
} ) ;
2026-01-05 22:52:13 +00:00
if ( fallbackThinking && ! aborted ) {
2026-01-05 18:54:23 +01:00
log . warn (
` unsupported thinking level for ${ provider } / ${ modelId } ; retrying with ${ fallbackThinking } ` ,
) ;
thinkLevel = fallbackThinking ;
continue ;
}
2025-12-25 23:50:52 +01:00
2026-01-05 22:52:13 +00:00
const fallbackConfigured =
2026-01-09 12:44:23 +00:00
( params . config ? . agents ? . defaults ? . model ? . fallbacks ? . length ? ? 0 ) >
0 ;
2026-01-06 00:56:29 +00:00
const authFailure = isAuthAssistantError ( lastAssistant ) ;
const rateLimitFailure = isRateLimitAssistantError ( lastAssistant ) ;
2026-01-09 21:31:13 +01:00
const failoverFailure = isFailoverAssistantError ( lastAssistant ) ;
const assistantFailoverReason = classifyFailoverReason (
lastAssistant ? . errorMessage ? ? "" ,
) ;
2026-01-08 18:34:08 -06:00
const cloudCodeAssistFormatError = lastAssistant ? . errorMessage
? isCloudCodeAssistFormatError ( lastAssistant . errorMessage )
: false ;
2026-01-06 07:18:06 +01:00
2026-01-06 04:48:34 +00:00
// Treat timeout as potential rate limit (Antigravity hangs on rate limit)
2026-01-10 01:25:01 +01:00
const shouldRotate = ( ! aborted && failoverFailure ) || timedOut ;
2026-01-06 07:18:06 +01:00
2026-01-06 04:48:34 +00:00
if ( shouldRotate ) {
2026-01-06 04:43:59 +00:00
// Mark current profile for cooldown before rotating
if ( lastProfileId ) {
2026-01-09 21:31:13 +01:00
const reason =
timedOut || assistantFailoverReason === "timeout"
? "timeout"
: ( assistantFailoverReason ? ? "unknown" ) ;
await markAuthProfileFailure ( {
2026-01-06 07:18:06 +01:00
store : authStore ,
profileId : lastProfileId ,
2026-01-09 21:31:13 +01:00
reason ,
2026-01-09 21:57:52 +01:00
cfg : params.config ,
2026-01-09 21:31:13 +01:00
agentDir : params.agentDir ,
2026-01-06 07:18:06 +01:00
} ) ;
2026-01-06 04:48:34 +00:00
if ( timedOut ) {
log . warn (
` Profile ${ lastProfileId } timed out (possible rate limit). Trying next account... ` ,
) ;
}
2026-01-08 18:34:08 -06:00
if ( cloudCodeAssistFormatError ) {
log . warn (
` Profile ${ lastProfileId } hit Cloud Code Assist format error. Tool calls will be sanitized on retry. ` ,
) ;
}
2026-01-06 04:43:59 +00:00
}
2026-01-06 00:56:29 +00:00
const rotated = await advanceAuthProfile ( ) ;
if ( rotated ) {
continue ;
}
2026-01-06 07:18:06 +01:00
if ( fallbackConfigured ) {
2026-01-06 00:56:29 +00:00
const message =
lastAssistant ? . errorMessage ? . trim ( ) ||
( lastAssistant
2026-01-10 20:28:34 +01:00
? formatAssistantErrorText ( lastAssistant , {
cfg : params.config ,
sessionKey : params.sessionKey ? ? params . sessionId ,
} )
2026-01-06 00:56:29 +00:00
: "" ) ||
2026-01-06 07:18:06 +01:00
( timedOut
? "LLM request timed out."
: rateLimitFailure
? "LLM request rate limited."
2026-01-09 21:31:13 +01:00
: authFailure
? "LLM request unauthorized."
: "LLM request failed." ) ;
2026-01-09 21:57:52 +01:00
const status =
resolveFailoverStatus ( assistantFailoverReason ? ? "unknown" ) ? ?
( isTimeoutErrorMessage ( message ) ? 408 : undefined ) ;
throw new FailoverError ( message , {
reason : assistantFailoverReason ? ? "unknown" ,
provider ,
model : modelId ,
profileId : lastProfileId ,
status ,
} ) ;
2026-01-06 00:56:29 +00:00
}
2026-01-05 22:52:13 +00:00
}
2026-01-05 18:54:23 +01:00
2026-01-06 18:51:45 +00:00
const usage = normalizeUsage ( lastAssistant ? . usage as UsageLike ) ;
2026-01-05 22:52:13 +00:00
const agentMeta : EmbeddedPiAgentMeta = {
sessionId : sessionIdUsed ,
provider : lastAssistant?.provider ? ? provider ,
model : lastAssistant?.model ? ? model . id ,
2026-01-06 18:51:45 +00:00
usage ,
2026-01-05 22:52:13 +00:00
} ;
2026-01-06 21:17:55 +01:00
const replyItems : Array < {
text : string ;
media? : string [ ] ;
isError? : boolean ;
2026-01-08 12:40:31 +00:00
audioAsVoice? : boolean ;
2026-01-10 03:01:04 +01:00
replyToId? : string ;
replyToTag? : boolean ;
replyToCurrent? : boolean ;
2026-01-06 21:17:55 +01:00
} > = [ ] ;
2026-01-05 22:52:13 +00:00
const errorText = lastAssistant
2026-01-10 20:28:34 +01:00
? formatAssistantErrorText ( lastAssistant , {
cfg : params.config ,
sessionKey : params.sessionKey ? ? params . sessionId ,
} )
2026-01-05 22:52:13 +00:00
: undefined ;
2026-01-06 21:17:55 +01:00
if ( errorText ) replyItems . push ( { text : errorText , isError : true } ) ;
2026-01-05 22:52:13 +00:00
const inlineToolResults =
params . verboseLevel === "on" &&
! params . onPartialReply &&
! params . onToolResult &&
toolMetas . length > 0 ;
if ( inlineToolResults ) {
for ( const { toolName , meta } of toolMetas ) {
const agg = formatToolAggregate ( toolName , meta ? [ meta ] : [ ] ) ;
2026-01-08 14:26:54 +00:00
const {
text : cleanedText ,
mediaUrls ,
audioAsVoice ,
2026-01-10 03:01:04 +01:00
replyToId ,
replyToTag ,
replyToCurrent ,
} = parseReplyDirectives ( agg ) ;
2026-01-05 22:52:13 +00:00
if ( cleanedText )
2026-01-08 14:26:54 +00:00
replyItems . push ( {
text : cleanedText ,
media : mediaUrls ,
audioAsVoice ,
2026-01-10 03:01:04 +01:00
replyToId ,
replyToTag ,
replyToCurrent ,
2026-01-08 14:26:54 +00:00
} ) ;
2026-01-05 22:52:13 +00:00
}
}
2026-01-05 18:04:36 +01:00
2026-01-10 00:02:13 +01:00
const reasoningText =
lastAssistant && params . reasoningLevel === "on"
2026-01-10 00:53:19 +01:00
? formatReasoningMessage ( extractAssistantThinking ( lastAssistant ) )
2026-01-10 00:02:13 +01:00
: "" ;
if ( reasoningText ) replyItems . push ( { text : reasoningText } ) ;
const fallbackAnswerText = lastAssistant
? extractAssistantText ( lastAssistant )
2026-01-07 06:16:38 +01:00
: "" ;
2026-01-10 00:02:13 +01:00
const answerTexts = assistantTexts . length
2026-01-05 22:52:13 +00:00
? assistantTexts
2026-01-10 00:02:13 +01:00
: fallbackAnswerText
? [ fallbackAnswerText ]
: [ ] ;
for ( const text of answerTexts ) {
2026-01-08 14:26:54 +00:00
const {
text : cleanedText ,
mediaUrls ,
audioAsVoice ,
2026-01-10 03:01:04 +01:00
replyToId ,
replyToTag ,
replyToCurrent ,
} = parseReplyDirectives ( text ) ;
2026-01-08 12:40:31 +00:00
if (
! cleanedText &&
( ! mediaUrls || mediaUrls . length === 0 ) &&
! audioAsVoice
)
2026-01-05 22:52:13 +00:00
continue ;
2026-01-08 14:26:54 +00:00
replyItems . push ( {
text : cleanedText ,
media : mediaUrls ,
audioAsVoice ,
2026-01-10 03:01:04 +01:00
replyToId ,
replyToTag ,
replyToCurrent ,
2026-01-08 14:26:54 +00:00
} ) ;
2025-12-25 23:50:52 +01:00
}
2025-12-22 20:45:22 +00:00
2026-01-08 12:40:31 +00:00
// Check if any replyItem has audioAsVoice tag - if so, apply to all media payloads
const hasAudioAsVoiceTag = replyItems . some (
( item ) = > item . audioAsVoice ,
) ;
2026-01-05 22:52:13 +00:00
const payloads = replyItems
. map ( ( item ) = > ( {
text : item.text?.trim ( ) ? item . text . trim ( ) : undefined ,
mediaUrls : item.media?.length ? item.media : undefined ,
mediaUrl : item.media?. [ 0 ] ,
2026-01-06 21:17:55 +01:00
isError : item.isError ,
2026-01-10 03:01:04 +01:00
replyToId : item.replyToId ,
replyToTag : item.replyToTag ,
replyToCurrent : item.replyToCurrent ,
2026-01-08 12:40:31 +00:00
// Apply audioAsVoice to media payloads if tag was found anywhere in response
audioAsVoice :
item . audioAsVoice || ( hasAudioAsVoiceTag && item . media ? . length ) ,
2026-01-05 22:52:13 +00:00
} ) )
. filter (
( p ) = >
p . text || p . mediaUrl || ( p . mediaUrls && p . mediaUrls . length > 0 ) ,
) ;
2025-12-22 20:45:22 +00:00
2026-01-05 22:52:13 +00:00
log . debug (
` embedded run done: runId= ${ params . runId } sessionId= ${ params . sessionId } durationMs= ${ Date . now ( ) - started } aborted= ${ aborted } ` ,
2025-12-25 23:50:52 +01:00
) ;
2026-01-06 01:08:36 +00:00
if ( lastProfileId ) {
2026-01-07 01:06:51 +01:00
await markAuthProfileGood ( {
2026-01-06 00:56:29 +00:00
store : authStore ,
provider ,
2026-01-06 01:08:36 +00:00
profileId : lastProfileId ,
2026-01-06 00:56:29 +00:00
} ) ;
2026-01-06 04:43:59 +00:00
// Track usage for round-robin rotation
2026-01-07 01:06:51 +01:00
await markAuthProfileUsed ( {
store : authStore ,
profileId : lastProfileId ,
} ) ;
2026-01-06 00:56:29 +00:00
}
2026-01-05 22:52:13 +00:00
return {
payloads : payloads.length ? payloads : undefined ,
meta : {
durationMs : Date.now ( ) - started ,
agentMeta ,
aborted ,
} ,
2026-01-07 03:24:56 -03:00
didSendViaMessagingTool : didSendViaMessagingTool ( ) ,
2026-01-08 00:50:29 +00:00
messagingToolSentTexts : getMessagingToolSentTexts ( ) ,
2026-01-08 08:49:16 +01:00
messagingToolSentTargets : getMessagingToolSentTargets ( ) ,
2026-01-05 22:52:13 +00:00
} ;
2026-01-05 18:54:23 +01:00
} finally {
restoreSkillEnv ? . ( ) ;
process . chdir ( prevCwd ) ;
}
2025-12-25 23:50:52 +01:00
}
} ) ,
) ;
2025-12-22 20:45:22 +00:00
}