diff --git a/src/web-dashboard-data.test.ts b/src/web-dashboard-data.test.ts index 3b3d6f5..2dd008b 100644 --- a/src/web-dashboard-data.test.ts +++ b/src/web-dashboard-data.test.ts @@ -442,6 +442,84 @@ describe('web dashboard room activity data', () => { ]); }); + it('hides legacy delivered work items without discord delivery evidence', () => { + const activity = buildWebDashboardRoomActivity({ + serviceId: 'codex-main', + entry: { + jid: 'dc:ops', + name: '#ops', + folder: 'ops', + agentType: 'codex', + status: 'inactive', + elapsedMs: null, + pendingMessages: false, + pendingTasks: 0, + }, + pairedTask: null, + turns: [], + attempts: [], + outputs: [], + outboundItems: [ + makeDeliveredWorkItem({ + id: 103, + delivery_role: null, + delivery_message_id: null, + result_payload: '서비스 재시작으로 이전 작업이 중단됐습니다.', + }), + ], + messages: [], + }); + + expect(activity.messages).toEqual([]); + }); + + it('keeps legacy delivered work items when a discord echo exists', () => { + const activity = buildWebDashboardRoomActivity({ + serviceId: 'codex-main', + entry: { + jid: 'dc:ops', + name: '#ops', + folder: 'ops', + agentType: 'codex', + status: 'inactive', + elapsedMs: null, + pendingMessages: false, + pendingTasks: 0, + }, + pairedTask: null, + turns: [], + attempts: [], + outputs: [], + outboundItems: [ + makeDeliveredWorkItem({ + id: 104, + delivery_message_id: null, + result_payload: 'TASK_DONE\n\nlegacy delivered output', + }), + ], + messages: [ + { + id: 'discord-legacy-echo', + chat_jid: 'dc:ops', + sender: 'bot-owner', + sender_name: '오너', + content: 'TASK_DONE\n\nlegacy delivered output', + timestamp: '2026-04-26T05:30:11.000Z', + is_from_me: true, + is_bot_message: true, + message_source_kind: 'bot', + }, + ], + }); + + expect(activity.messages).toEqual([ + expect.objectContaining({ + id: 'work:104', + content: 'TASK_DONE\n\nlegacy delivered output', + }), + ]); + }); + it('merges canonical outbound and recent messages in chronological order', () => { const activity = buildWebDashboardRoomActivity({ serviceId: 'codex-main', diff --git a/src/web-dashboard-data.ts b/src/web-dashboard-data.ts index 5d0785e..aaf1b44 100644 --- a/src/web-dashboard-data.ts +++ b/src/web-dashboard-data.ts @@ -575,6 +575,15 @@ function isDuplicateOfCanonicalOutbound( }); } +function hasDiscordEchoForCanonicalOutbound( + canonical: WebDashboardRoomMessage, + messages: WebDashboardRoomMessage[], +): boolean { + return messages.some((message) => + isDuplicateOfCanonicalOutbound(message, [canonical]), + ); +} + function compareRoomMessagesByTimestamp( a: WebDashboardRoomMessage, b: WebDashboardRoomMessage, @@ -670,19 +679,36 @@ export function buildWebDashboardRoomActivity(args: { ? (latestAttemptByTurnId.get(currentTurn.turn_id) ?? null) : null; const outputLimit = args.outputLimit ?? 4; - const canonicalOutboundMessages = (args.outboundItems ?? []) - .map(sanitizeCanonicalOutboundMessage) + const sanitizedRecentMessages = args.messages + .filter((message) => !isTaskStatusMessage(message)) + .map(sanitizeRoomMessage) .filter((message): message is WebDashboardRoomMessage => Boolean(message)); + const canonicalOutboundMessages = (args.outboundItems ?? []) + .map((item) => ({ + item, + message: sanitizeCanonicalOutboundMessage(item), + })) + .filter( + ( + candidate, + ): candidate is { + item: WorkItem; + message: WebDashboardRoomMessage; + } => Boolean(candidate.message), + ) + .filter( + ({ item, message }) => + Boolean(item.delivery_message_id) || + hasDiscordEchoForCanonicalOutbound(message, sanitizedRecentMessages), + ) + .map(({ message }) => message); const canonicalDeliveryMessageIds = new Set( (args.outboundItems ?? []) .map((item) => item.delivery_message_id) .filter((id): id is string => Boolean(id)), ); - const recentMessages = args.messages + const recentMessages = sanitizedRecentMessages .filter((message) => !canonicalDeliveryMessageIds.has(message.id)) - .filter((message) => !isTaskStatusMessage(message)) - .map(sanitizeRoomMessage) - .filter((message): message is WebDashboardRoomMessage => Boolean(message)) .filter( (message) => !isDuplicateOfCanonicalOutbound(message, canonicalOutboundMessages),