@@ -2050,6 +2050,7 @@ export async function dispatchReplyFromConfig(
20502050 suppressHookUserDelivery,
20512051 suppressHookReplyLifecycle,
20522052 } = sourceReplyPolicy ;
2053+ const reasoningPayloadsEnabled = params . replyOptions ?. reasoningPayloadsEnabled === true ;
20532054 const attachSourceReplyDeliveryMode = (
20542055 result : DispatchFromConfigResult ,
20552056 ) : DispatchFromConfigResult =>
@@ -2481,16 +2482,19 @@ export async function dispatchReplyFromConfig(
24812482 markInboundDedupeReplayUnsafe ( ) ;
24822483 finalReplyDeliveryStarted = true ;
24832484 }
2484- const ttsPayload = await maybeApplyTtsToReplyPayload ( {
2485- payload,
2486- cfg,
2487- channel : deliveryChannel ,
2488- kind : "final" ,
2489- inboundAudio,
2490- ttsAuto : sessionTtsAuto ,
2491- agentId : sessionAgentId ,
2492- accountId : replyRoute . accountId ,
2493- } ) ;
2485+ const ttsPayload =
2486+ payload . isReasoning === true
2487+ ? payload
2488+ : await maybeApplyTtsToReplyPayload ( {
2489+ payload,
2490+ cfg,
2491+ channel : deliveryChannel ,
2492+ kind : "final" ,
2493+ inboundAudio,
2494+ ttsAuto : sessionTtsAuto ,
2495+ agentId : sessionAgentId ,
2496+ accountId : replyRoute . accountId ,
2497+ } ) ;
24942498 throwIfFinalDeliveryAborted ( ) ;
24952499 const normalizedPayload = await normalizeReplyMediaPayload ( ttsPayload ) ;
24962500 throwIfFinalDeliveryAborted ( ) ;
@@ -3078,6 +3082,7 @@ export async function dispatchReplyFromConfig(
30783082 deliverStandaloneCommentaryProgress ||
30793083 canForwardSuppressedSourceItemEvents ||
30803084 params . replyOptions ?. commentaryProgressEnabled ,
3085+ reasoningPayloadsEnabled,
30813086 onCommandOutput : wrapProgressCallback ( params . replyOptions ?. onCommandOutput , {
30823087 forwardWhenSourceDeliverySuppressed : true ,
30833088 requiresToolSummaryVisibility : true ,
@@ -3306,17 +3311,16 @@ export async function dispatchReplyFromConfig(
33063311 if ( suppressDelivery ) {
33073312 return ;
33083313 }
3309- // Suppress reasoning payloads — channels using this generic dispatch
3310- // path (WhatsApp, web, etc.) do not have a dedicated reasoning lane.
3311- // Telegram has its own dispatch path that handles reasoning splitting.
3312- if ( payload . isReasoning === true ) {
3314+ // Durable reasoning is a channel-owned lane; generic channels
3315+ // keep the historical suppression unless they explicitly opt in.
3316+ if ( payload . isReasoning === true && ! reasoningPayloadsEnabled ) {
33133317 return ;
33143318 }
33153319 // Accumulate block text for TTS generation after streaming.
33163320 // Exclude status notices — they are informational UI signals
33173321 // and must not be synthesised into the spoken reply.
33183322 const isStatusNotice = isReplyPayloadStatusNotice ( payload ) ;
3319- if ( payload . text && ! isStatusNotice ) {
3323+ if ( payload . text && ! isStatusNotice && payload . isReasoning !== true ) {
33203324 const joinsBufferedTtsDirective =
33213325 cleanBlockTtsDirectiveText ?. hasBufferedDirectiveText ( ) === true ;
33223326 if ( accumulatedBlockText . length > 0 ) {
@@ -3330,7 +3334,10 @@ export async function dispatchReplyFromConfig(
33303334 blockCount ++ ;
33313335 }
33323336 const visiblePayload =
3333- payload . text && cleanBlockTtsDirectiveText && ! isStatusNotice
3337+ payload . text &&
3338+ cleanBlockTtsDirectiveText &&
3339+ ! isStatusNotice &&
3340+ payload . isReasoning !== true
33343341 ? ( ( ) => {
33353342 const text = cleanBlockTtsDirectiveText . push ( payload . text ) ;
33363343 return copyReplyPayloadMetadata ( payload , {
@@ -3359,16 +3366,19 @@ export async function dispatchReplyFromConfig(
33593366 if ( isDispatchOperationAborted ( ) ) {
33603367 return ;
33613368 }
3362- const ttsPayload = await maybeApplyTtsToReplyPayload ( {
3363- payload : visiblePayload ,
3364- cfg,
3365- channel : deliveryChannel ,
3366- kind : "block" ,
3367- inboundAudio,
3368- ttsAuto : sessionTtsAuto ,
3369- agentId : sessionAgentId ,
3370- accountId : replyRoute . accountId ,
3371- } ) ;
3369+ const ttsPayload =
3370+ payload . isReasoning === true
3371+ ? visiblePayload
3372+ : await maybeApplyTtsToReplyPayload ( {
3373+ payload : visiblePayload ,
3374+ cfg,
3375+ channel : deliveryChannel ,
3376+ kind : "block" ,
3377+ inboundAudio,
3378+ ttsAuto : sessionTtsAuto ,
3379+ agentId : sessionAgentId ,
3380+ accountId : replyRoute . accountId ,
3381+ } ) ;
33723382 const normalizedPayload = await normalizeReplyMediaPayload ( ttsPayload ) ;
33733383 if ( isDispatchOperationAborted ( ) ) {
33743384 return ;
@@ -3479,9 +3489,9 @@ export async function dispatchReplyFromConfig(
34793489 ( ctx . InboundEventKind !== "room_event" || explicitCommandTurnCtx ) ;
34803490 for ( const [ replyIndex , reply ] of replies . entries ( ) ) {
34813491 throwIfDispatchOperationAborted ( ) ;
3482- // Suppress reasoning payloads from channel delivery — channels using this
3483- // generic dispatch path do not have a dedicated reasoning lane .
3484- if ( reply . isReasoning === true ) {
3492+ // Durable reasoning is a channel-owned lane; generic channels keep the
3493+ // historical suppression unless they explicitly opt in .
3494+ if ( reply . isReasoning === true && ! reasoningPayloadsEnabled ) {
34853495 continue ;
34863496 }
34873497 if ( suppressDelivery && ! shouldDeliverDespiteSourceReplySuppression ( reply ) ) {
0 commit comments