@@ -53,6 +53,9 @@ export interface BridgeAgentOptions {
5353const DEVICE_BINDING_PREFIX = 'device:' ;
5454const THREAD_CACHE_TTL_MS = 10 * 60 * 1000 ;
5555const THREAD_LIST_LIMIT = 20 ;
56+ const THREAD_STATUS_PROBE_LIMIT = 10 ;
57+ const THREAD_STATUS_PROBE_TIMEOUT_MS = 6_000 ;
58+ const THREAD_STATUS_PROBE_BATCH_SIZE = 4 ;
5659const GLOBAL_STATE_CACHE_TTL_MS = 15_000 ;
5760const CODEX_GLOBAL_STATE_PATH = process . env . CODEX_GLOBAL_STATE_PATH
5861 || ( process . env . HOME ? path . join ( process . env . HOME , '.codex' , '.codex-global-state.json' ) : '' ) ;
@@ -92,7 +95,7 @@ interface DisplayThreadsState {
9295 usingSidebarVisibility : boolean ;
9396}
9497
95- type ThreadTaskState = 'running' | 'queued' | 'unknown' ;
98+ type ThreadTaskState = 'running' | 'queued' | 'idle' | ' unknown';
9699
97100interface TurnConversationSummary {
98101 turnId : string ;
@@ -463,7 +466,13 @@ function buildThreadButtonTitle(
463466 taskState : ThreadTaskState ,
464467) : string {
465468 const title = truncate ( compactText ( item . title || localeText ( locale , `会话 ${ index + 1 } ` , `Thread ${ index + 1 } ` ) ) , 24 ) ;
466- const status = taskState === 'running' ? '⏳' : taskState === 'queued' ? '🕒' : '⚪' ;
469+ const status = taskState === 'running'
470+ ? '⏳'
471+ : taskState === 'queued'
472+ ? '🕒'
473+ : taskState === 'idle'
474+ ? '💤'
475+ : '⚪' ;
467476 return `${ isCurrent ? '✅ ' : '' } ${ status } 🧵 ${ title } ` ;
468477}
469478
@@ -985,9 +994,75 @@ export class BridgeAgent extends EventEmitter {
985994 if ( taskState === 'queued' ) {
986995 return this . t ( '🕒 排队中' , '🕒 Queued' ) ;
987996 }
997+ if ( taskState === 'idle' ) {
998+ return this . t ( '💤 空闲' , '💤 Idle' ) ;
999+ }
9881000 return this . t ( '⚪ 未观测' , '⚪ Unobserved' ) ;
9891001 }
9901002
1003+ private inferThreadTaskStateFromSnapshot ( snapshot : ThreadConversationSnapshot | null ) : ThreadTaskState {
1004+ if ( ! snapshot || ! Array . isArray ( snapshot . recentTurns ) || snapshot . recentTurns . length === 0 ) {
1005+ return 'unknown' ;
1006+ }
1007+ const statuses = snapshot . recentTurns
1008+ . slice ( - 3 )
1009+ . map ( ( turn ) => String ( turn . status || '' ) . toLowerCase ( ) ) ;
1010+
1011+ if ( statuses . some ( ( status ) => status === 'inprogress' || status === 'in_progress' || status === 'running' ) ) {
1012+ return 'running' ;
1013+ }
1014+ if ( statuses . some ( ( status ) => status === 'queued' || status === 'pending' ) ) {
1015+ return 'queued' ;
1016+ }
1017+ if ( statuses . some ( ( status ) => status === 'completed' || status === 'failed' || status === 'cancelled' || status === 'canceled' ) ) {
1018+ return 'idle' ;
1019+ }
1020+ return 'unknown' ;
1021+ }
1022+
1023+ private async probeThreadTaskStates ( threads : ThreadSummary [ ] ) : Promise < Map < string , ThreadTaskState > > {
1024+ const states = new Map < string , ThreadTaskState > ( ) ;
1025+ if ( threads . length === 0 ) {
1026+ return states ;
1027+ }
1028+
1029+ for ( const thread of threads ) {
1030+ const runtimeState = this . getThreadTaskState ( thread . id ) ;
1031+ if ( runtimeState !== 'unknown' ) {
1032+ states . set ( thread . id , runtimeState ) ;
1033+ }
1034+ }
1035+
1036+ const toProbe = threads
1037+ . filter ( ( thread ) => ! states . has ( thread . id ) )
1038+ . slice ( 0 , THREAD_STATUS_PROBE_LIMIT ) ;
1039+ if ( toProbe . length === 0 ) {
1040+ return states ;
1041+ }
1042+
1043+ for ( let i = 0 ; i < toProbe . length ; i += THREAD_STATUS_PROBE_BATCH_SIZE ) {
1044+ const batch = toProbe . slice ( i , i + THREAD_STATUS_PROBE_BATCH_SIZE ) ;
1045+ const settled = await Promise . allSettled (
1046+ batch . map ( async ( thread ) => {
1047+ const snapshot = await withTimeout (
1048+ this . fetchThreadConversationSnapshot ( thread . id ) ,
1049+ THREAD_STATUS_PROBE_TIMEOUT_MS ,
1050+ `thread-status-probe:${ thread . id } ` ,
1051+ ) ;
1052+ return { threadId : thread . id , taskState : this . inferThreadTaskStateFromSnapshot ( snapshot ) } ;
1053+ } ) ,
1054+ ) ;
1055+
1056+ for ( const item of settled ) {
1057+ if ( item . status === 'fulfilled' ) {
1058+ states . set ( item . value . threadId , item . value . taskState ) ;
1059+ }
1060+ }
1061+ }
1062+
1063+ return states ;
1064+ }
1065+
9911066 async handleIncomingMessage ( event : IncomingUserMessageEvent ) : Promise < void > {
9921067 const fallbackCommand = this . parseControlCommandFromText ( event . text ) ;
9931068 if ( fallbackCommand ) {
@@ -1573,6 +1648,9 @@ export class BridgeAgent extends EventEmitter {
15731648 return ;
15741649 }
15751650
1651+ const taskStates = await this . probeThreadTaskStates ( displayItems . map ( ( item ) => item . thread ) ) ;
1652+ const resolveTaskState = ( threadId : string ) : ThreadTaskState => taskStates . get ( threadId ) || 'unknown' ;
1653+
15761654 this . recentThreadsByChat . set ( event . chatId , {
15771655 threads : displayItems . map ( ( item ) => item . thread ) ,
15781656 updatedAt : nowMs ( ) ,
@@ -1602,7 +1680,7 @@ export class BridgeAgent extends EventEmitter {
16021680 lines . push ( `<b>📁 ${ escapeTelegramHtml ( currentGroup ) } </b>` ) ;
16031681 }
16041682 lines . push (
1605- `${ index + 1 } . ${ isCurrent ? '✅ ' : '' } <b>${ escapeTelegramHtml ( item . title ) } </b> ${ escapeTelegramHtml ( this . formatThreadTaskStateLabel ( this . getThreadTaskState ( thread . id ) ) ) } ` ,
1683+ `${ index + 1 } . ${ isCurrent ? '✅ ' : '' } <b>${ escapeTelegramHtml ( item . title ) } </b> ${ escapeTelegramHtml ( this . formatThreadTaskStateLabel ( resolveTaskState ( thread . id ) ) ) } ` ,
16061684 ) ;
16071685 lines . push (
16081686 this . locale === 'en'
@@ -1616,7 +1694,7 @@ export class BridgeAgent extends EventEmitter {
16161694 if ( state . usingSidebarVisibility && state . hiddenCount > 0 ) {
16171695 lines . push ( this . locale === 'en' ? `Filtered sidebar-invisible threads: ${ state . hiddenCount } ` : `已过滤侧边栏不可见会话: ${ state . hiddenCount } ` ) ;
16181696 }
1619- lines . push ( this . t ( '状态说明:仅⏳/🕒表示桥接中任务;⚪表示当前未观测到桥接任务 。' , 'Status note: only ⏳/🕒 indicate bridge tasks; ⚪ means no bridge task currently observed .' ) ) ;
1697+ lines . push ( this . t ( '状态说明:⏳运行中 / 🕒排队中 / 💤空闲 / ⚪未观测 。' , 'Status note: ⏳ running / 🕒 queued / 💤 idle / ⚪ unobserved .' ) ) ;
16201698 lines . push ( '' ) ;
16211699 lines . push ( this . t ( '可用: /bind [编号] | /detail [编号] | /bind latest' , 'Available: /bind [index] | /detail [index] | /bind latest' ) ) ;
16221700 lines . push ( this . t ( '快速查看当前: /active' , 'Quick view current: /active' ) ) ;
@@ -1627,7 +1705,7 @@ export class BridgeAgent extends EventEmitter {
16271705 displayItems ,
16281706 state . currentBinding ?. threadId || null ,
16291707 this . locale ,
1630- ( threadId ) => this . getThreadTaskState ( threadId ) ,
1708+ ( threadId ) => resolveTaskState ( threadId ) ,
16311709 ) ,
16321710 parseMode : 'HTML' ,
16331711 } ) ;
0 commit comments