Skip to content

Commit d2f3bed

Browse files
committed
feat: probe thread states in /threads via includeTurns
1 parent bbf9188 commit d2f3bed

1 file changed

Lines changed: 83 additions & 5 deletions

File tree

packages/bridge-core/src/agent.ts

Lines changed: 83 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ export interface BridgeAgentOptions {
5353
const DEVICE_BINDING_PREFIX = 'device:';
5454
const THREAD_CACHE_TTL_MS = 10 * 60 * 1000;
5555
const 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;
5659
const GLOBAL_STATE_CACHE_TTL_MS = 15_000;
5760
const 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

97100
interface 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

Comments
 (0)