@@ -4,8 +4,14 @@ import { join } from 'node:path';
44import type { HostApiContext } from '../context' ;
55import { parseJsonBody , sendJson } from '../route-utils' ;
66import { getOpenClawConfigDir } from '../../utils/paths' ;
7+ import { resolveAccountIdFromSessionHistory } from '../../utils/session-util' ;
78import { toOpenClawChannelType , toUiChannelType } from '../../utils/channel-alias' ;
9+ import { resolveAgentIdFromChannel } from '../../utils/agent-config' ;
810
11+ /**
12+ * Find agentId from session history by delivery "to" address.
13+ * Efficiently searches only agent session directories for matching deliveryContext.to.
14+ */
915interface GatewayCronJob {
1016 id : string ;
1117 name : string ;
@@ -461,6 +467,14 @@ export async function handleCronRoutes(
461467 const result = await ctx . gatewayManager . rpc ( 'cron.list' , { includeDisabled : true } , 8000 ) ;
462468 const data = result as { jobs ?: GatewayCronJob [ ] } ;
463469 jobs = data ?. jobs ?? ( Array . isArray ( result ) ? result as GatewayCronJob [ ] : [ ] ) ;
470+
471+ // DEBUG: log name and agentId for each job
472+ console . debug ( 'Fetched cron jobs from Gateway:' ) ;
473+ for ( const job of jobs ) {
474+ const jobAgentId = ( job as unknown as { agentId ?: string } ) . agentId ;
475+ const deliveryInfo = job . delivery ? `delivery={mode:${ job . delivery . mode } , channel:${ job . delivery . channel || '(none)' } , accountId:${ job . delivery . accountId || '(none)' } , to:${ job . delivery . to || '(none)' } }` : 'delivery=(none)' ;
476+ console . debug ( ` - name: "${ job . name } ", agentId: "${ jobAgentId || '(undefined)' } ", ${ deliveryInfo } , sessionTarget: "${ job . sessionTarget || '(none)' } ", payload.kind: "${ job . payload ?. kind || '(none)' } "` ) ;
477+ }
464478 } catch {
465479 // Fallback: read cron.json directly when Gateway RPC fails/times out.
466480 try {
@@ -477,7 +491,8 @@ export async function handleCronRoutes(
477491
478492 // Run repair in background — don't block the response.
479493 if ( ! usedFallback && jobs . length > 0 ) {
480- const jobsToRepair = jobs . filter ( ( job ) => {
494+ // Repair 1: delivery channel missing
495+ const jobsToRepairDelivery = jobs . filter ( ( job ) => {
481496 const isIsolatedAgent =
482497 ( job . sessionTarget === 'isolated' || ! job . sessionTarget ) &&
483498 job . payload ?. kind === 'agentTurn' ;
@@ -487,10 +502,10 @@ export async function handleCronRoutes(
487502 ! job . delivery ?. channel
488503 ) ;
489504 } ) ;
490- if ( jobsToRepair . length > 0 ) {
505+ if ( jobsToRepairDelivery . length > 0 ) {
491506 // Fire-and-forget: repair in background
492507 void ( async ( ) => {
493- for ( const job of jobsToRepair ) {
508+ for ( const job of jobsToRepairDelivery ) {
494509 try {
495510 await ctx . gatewayManager . rpc ( 'cron.update' , {
496511 id : job . id ,
@@ -502,14 +517,76 @@ export async function handleCronRoutes(
502517 }
503518 } ) ( ) ;
504519 // Optimistically fix the response data
505- for ( const job of jobsToRepair ) {
520+ for ( const job of jobsToRepairDelivery ) {
506521 job . delivery = { mode : 'none' } ;
507522 if ( job . state ?. lastError ?. includes ( 'Channel is required' ) ) {
508523 job . state . lastError = undefined ;
509524 job . state . lastStatus = 'ok' ;
510525 }
511526 }
512527 }
528+
529+ // Repair 2: agentId is undefined for jobs with announce delivery
530+ // Only repair undefined -> inferred agent, NOT main -> inferred agent
531+ const jobsToRepairAgent = jobs . filter ( ( job ) => {
532+ const jobAgentId = ( job as unknown as { agentId ?: string } ) . agentId ;
533+ return (
534+ ( job . sessionTarget === 'isolated' || ! job . sessionTarget ) &&
535+ job . payload ?. kind === 'agentTurn' &&
536+ job . delivery ?. mode === 'announce' &&
537+ job . delivery ?. channel &&
538+ jobAgentId === undefined // Only repair when agentId is completely undefined
539+ ) ;
540+ } ) ;
541+ if ( jobsToRepairAgent . length > 0 ) {
542+ console . debug ( `Found ${ jobsToRepairAgent . length } jobs needing agent repair:` ) ;
543+ for ( const job of jobsToRepairAgent ) {
544+ console . debug ( ` - Job "${ job . name } " (id: ${ job . id } ): current agentId="${ ( job as unknown as { agentId ?: string } ) . agentId || '(undefined)' } ", channel="${ job . delivery ?. channel } ", accountId="${ job . delivery ?. accountId || '(none)' } "` ) ;
545+ }
546+ // Fire-and-forget: repair in background
547+ void ( async ( ) => {
548+ for ( const job of jobsToRepairAgent ) {
549+ try {
550+ const channel = toOpenClawChannelType ( job . delivery ! . channel ! ) ;
551+ const accountId = job . delivery ! . accountId ;
552+ const toAddress = job . delivery ! . to ;
553+
554+ // Try 1: resolve from channel + accountId binding
555+ let correctAgentId = await resolveAgentIdFromChannel ( channel , accountId ) ;
556+
557+ // If no accountId, try to resolve it from session history using "to" address, then get agentId
558+ let resolvedAccountId : string | null = null ;
559+ if ( ! correctAgentId && ! accountId && toAddress ) {
560+ console . debug ( `No binding found for channel="${ channel } ", accountId="${ accountId || '(none)' } ", trying session history for to="${ toAddress } "` ) ;
561+ resolvedAccountId = await resolveAccountIdFromSessionHistory ( toAddress , channel ) ;
562+ if ( resolvedAccountId ) {
563+ console . debug ( `Resolved accountId="${ resolvedAccountId } " from session history, now resolving agentId` ) ;
564+ correctAgentId = await resolveAgentIdFromChannel ( channel , resolvedAccountId ) ;
565+ }
566+ }
567+
568+ if ( correctAgentId ) {
569+ console . debug ( `Repairing job "${ job . name } ": agentId "${ ( job as unknown as { agentId ?: string } ) . agentId || '(undefined)' } " -> "${ correctAgentId } "` ) ;
570+ // When accountId was resolved via to address, include it in the patch
571+ const patch : Record < string , unknown > = { agentId : correctAgentId } ;
572+ if ( resolvedAccountId && ! accountId ) {
573+ patch . delivery = { accountId : resolvedAccountId } ;
574+ }
575+ await ctx . gatewayManager . rpc ( 'cron.update' , { id : job . id , patch } ) ;
576+ // Update the local job object so response reflects correct agentId
577+ ( job as unknown as { agentId : string } ) . agentId = correctAgentId ;
578+ if ( resolvedAccountId && ! accountId && job . delivery ) {
579+ job . delivery . accountId = resolvedAccountId ;
580+ }
581+ } else {
582+ console . warn ( `Could not resolve agent for job "${ job . name } ": channel="${ channel } ", accountId="${ accountId || '(none)' } ", to="${ toAddress || '(none)' } "` ) ;
583+ }
584+ } catch ( error ) {
585+ console . error ( `Failed to repair agent for job "${ job . name } ":` , error ) ;
586+ }
587+ }
588+ } ) ( ) ;
589+ }
513590 }
514591
515592 sendJson ( res , 200 , jobs . map ( ( job ) => ( { ...transformCronJob ( job ) , ...( usedFallback ? { _fromFallback : true } : { } ) } ) ) ) ;
@@ -532,6 +609,8 @@ export async function handleCronRoutes(
532609 const agentId = typeof input . agentId === 'string' && input . agentId . trim ( )
533610 ? input . agentId . trim ( )
534611 : 'main' ;
612+ // DEBUG: log the input and resolved agentId
613+ console . debug ( `Creating cron job: name="${ input . name } ", input.agentId="${ input . agentId || '(not provided)' } ", resolved agentId="${ agentId } "` ) ;
535614 const delivery = normalizeCronDelivery ( input . delivery ) ;
536615 const unsupportedDeliveryError = getUnsupportedCronDeliveryError ( delivery . channel ) ;
537616 if ( delivery . mode === 'announce' && unsupportedDeliveryError ) {
0 commit comments