@@ -19,6 +19,8 @@ import type { TelegramMessageContextOptions } from "./bot-message-context.types.
1919import type { TelegramPromptContextEntry } from "./bot-message-context.types.js" ;
2020import { dispatchTelegramMessage } from "./bot-message-dispatch.js" ;
2121import {
22+ createTelegramSpooledReplayDeferredParticipant ,
23+ getTelegramSpooledReplayDeferredParticipant ,
2224 isTelegramSpooledReplayUpdate ,
2325 recordTelegramMessageProcessingResult ,
2426 type TelegramMessageProcessingResult ,
@@ -243,55 +245,103 @@ export const createTelegramMessageProcessor = (deps: TelegramMessageProcessorDep
243245 await turnContext . onDispatchStart ?.( ) ;
244246 const spooledReplay =
245247 options ?. spooledReplay === true || isTelegramSpooledReplayUpdate ( primaryCtx . update ) ;
246- try {
247- const dispatchResult = await dispatchTelegramMessage ( {
248- context,
249- bot,
250- cfg : context . cfg ,
251- runtime,
252- replyToMode : turnSettings . replyToMode ,
253- streamMode : turnSettings . streamMode ,
254- textLimit : turnSettings . textLimit ,
255- telegramCfg : turnTelegramCfg ,
256- telegramDeps,
257- opts,
258- retryDispatchErrors : spooledReplay ,
259- suppressFailureFallback : spooledReplay ,
260- } ) ;
261- if ( dispatchResult ?. kind === "failed-retryable" ) {
248+
249+ const runDispatch = async ( params : {
250+ onTurnAdopted ?: ( ) => void | Promise < void > ;
251+ } ) : Promise < TelegramMessageProcessingResult > => {
252+ try {
253+ const dispatchResult = await dispatchTelegramMessage ( {
254+ context,
255+ bot,
256+ cfg : context . cfg ,
257+ runtime,
258+ replyToMode : turnSettings . replyToMode ,
259+ streamMode : turnSettings . streamMode ,
260+ textLimit : turnSettings . textLimit ,
261+ telegramCfg : turnTelegramCfg ,
262+ telegramDeps,
263+ opts,
264+ retryDispatchErrors : spooledReplay ,
265+ suppressFailureFallback : spooledReplay ,
266+ onTurnAdopted : params . onTurnAdopted ,
267+ } ) ;
268+ if ( dispatchResult ?. kind === "failed-retryable" ) {
269+ const result : TelegramMessageProcessingResult = {
270+ kind : "failed-retryable" ,
271+ error : dispatchResult . error ,
272+ } ;
273+ recordCurrentUpdateProcessingResult ( result ) ;
274+ return result ;
275+ }
276+ if ( ingressDebugEnabled && ingressReceivedAtMs ) {
277+ logVerbose (
278+ `telegram ingress: chatId=${ context . chatId } dispatchCompleteMs=${ Date . now ( ) - ingressReceivedAtMs } ` +
279+ ( options ?. ingressBuffer ? ` buffer=${ options . ingressBuffer } ` : "" ) ,
280+ ) ;
281+ }
282+ const result : TelegramMessageProcessingResult = { kind : "completed" } ;
283+ recordCurrentUpdateProcessingResult ( result ) ;
284+ return result ;
285+ } catch ( err ) {
286+ runtime . error ?.( danger ( `telegram message processing failed: ${ String ( err ) } ` ) ) ;
287+ if ( ! spooledReplay ) {
288+ try {
289+ await bot . api . sendMessage (
290+ context . chatId ,
291+ "Something went wrong while processing your request. Please try again." ,
292+ buildTelegramThreadParams ( context . threadSpec ) ,
293+ ) ;
294+ } catch { }
295+ }
262296 const result : TelegramMessageProcessingResult = {
263297 kind : "failed-retryable" ,
264- error : dispatchResult . error ,
298+ error : err ,
265299 } ;
266300 recordCurrentUpdateProcessingResult ( result ) ;
267301 return result ;
268302 }
269- if ( ingressDebugEnabled && ingressReceivedAtMs ) {
270- logVerbose (
271- `telegram ingress: chatId=${ context . chatId } dispatchCompleteMs=${ Date . now ( ) - ingressReceivedAtMs } ` +
272- ( options ?. ingressBuffer ? ` buffer=${ options . ingressBuffer } ` : "" ) ,
303+ } ;
304+
305+ // Spooled ingress: complete the spool row at turn adoption (recovery state
306+ // persisted), not settle. The deferred participant hands ownership back to
307+ // the spool drain so the per-chat lane frees while the agent turn continues.
308+ if ( spooledReplay ) {
309+ const existingParticipant = getTelegramSpooledReplayDeferredParticipant ( ) ;
310+ const participant =
311+ existingParticipant ??
312+ createTelegramSpooledReplayDeferredParticipant (
313+ `agent-turn:${ context . chatId } :${ context . ctxPayload . MessageSid ?? Date . now ( ) } ` ,
273314 ) ;
315+ if ( participant ) {
316+ let adopted = false ;
317+ const settleIfNeeded = ( result : TelegramMessageProcessingResult ) => {
318+ if ( adopted ) {
319+ return ;
320+ }
321+ participant . settle ( result ) ;
322+ } ;
323+ const run = async ( ) => {
324+ const result = await runDispatch ( {
325+ onTurnAdopted : async ( ) => {
326+ if ( adopted ) {
327+ return ;
328+ }
329+ adopted = true ;
330+ participant . settle ( { kind : "completed" } ) ;
331+ } ,
332+ } ) ;
333+ settleIfNeeded ( result ) ;
334+ return result ;
335+ } ;
336+ if ( existingParticipant ) {
337+ return await run ( ) ;
338+ }
339+ void run ( ) ;
340+ const detached : TelegramMessageProcessingResult = { kind : "completed" } ;
341+ return detached ;
274342 }
275- const result : TelegramMessageProcessingResult = { kind : "completed" } ;
276- recordCurrentUpdateProcessingResult ( result ) ;
277- return result ;
278- } catch ( err ) {
279- runtime . error ?.( danger ( `telegram message processing failed: ${ String ( err ) } ` ) ) ;
280- if ( ! spooledReplay ) {
281- try {
282- await bot . api . sendMessage (
283- context . chatId ,
284- "Something went wrong while processing your request. Please try again." ,
285- buildTelegramThreadParams ( context . threadSpec ) ,
286- ) ;
287- } catch { }
288- }
289- const result : TelegramMessageProcessingResult = {
290- kind : "failed-retryable" ,
291- error : err ,
292- } ;
293- recordCurrentUpdateProcessingResult ( result ) ;
294- return result ;
295343 }
344+
345+ return await runDispatch ( { } ) ;
296346 } ;
297347} ;
0 commit comments