From 1417c370423c2f1cbe9f6c88eb951bc90f068824 Mon Sep 17 00:00:00 2001 From: ejclaw Date: Tue, 28 Apr 2026 22:13:24 +0900 Subject: [PATCH 1/3] Extract message runtime handoff helpers --- quality/code-quality-budgets.json | 4 - src/message-runtime-handoffs.ts | 119 +++++++++++++++++++----------- 2 files changed, 75 insertions(+), 48 deletions(-) diff --git a/quality/code-quality-budgets.json b/quality/code-quality-budgets.json index b812b6b..9b7f93d 100644 --- a/quality/code-quality-budgets.json +++ b/quality/code-quality-budgets.json @@ -141,10 +141,6 @@ "maxFunctionLines": 239, "owner": "message executor hotspot" }, - "src/message-runtime-handoffs.ts": { - "maxFunctionLines": 207, - "owner": "message runtime handoffs hotspot" - }, "src/message-runtime-queue.test.ts": { "maxFunctionLines": 366, "owner": "message runtime queue test hotspot" diff --git a/src/message-runtime-handoffs.ts b/src/message-runtime-handoffs.ts index 20784bd..7d81156 100644 --- a/src/message-runtime-handoffs.ts +++ b/src/message-runtime-handoffs.ts @@ -19,7 +19,12 @@ import { import { buildPairedTurnIdentity } from './paired-turn-identity.js'; import type { ScheduledPairedFollowUpIntentKind } from './paired-follow-up-scheduler.js'; import type { ExecuteTurnFn } from './message-runtime-types.js'; -import type { Channel, PairedTask, RegisteredGroup } from './types.js'; +import type { + Channel, + PairedRoomRole, + PairedTask, + RegisteredGroup, +} from './types.js'; export function enqueuePendingHandoffs(args: { enqueueTask: ( @@ -158,6 +163,61 @@ function failClaimedHandoff(args: { requeueFailedClaimedPairedTurn(args); } +function resolveHandoffDeliveryChannel(args: { + handoff: ServiceHandoff; + handoffRole: PairedRoomRole; + fallbackChannel: Channel; + channels: Channel[]; + getPairedTaskById?: + | (( + id: string, + ) => + | Pick + | undefined) + | undefined; + enqueueMessageCheck?: ((chatJid: string) => void) | undefined; +}): Channel | undefined { + const { handoff, handoffRole } = args; + if (handoffRole === 'owner') { + return args.fallbackChannel; + } + + const roleChannel = findChannelByName( + args.channels, + getFixedRoleChannelName(handoffRole), + ); + if (!roleChannel) { + failClaimedHandoff({ + handoff, + error: getMissingRoleChannelMessage(handoffRole), + getPairedTaskById: args.getPairedTaskById, + enqueueMessageCheck: args.enqueueMessageCheck, + }); + return undefined; + } + return roleChannel; +} + +function buildHandoffPairedTurnIdentity( + handoff: ServiceHandoff, + handoffRole: PairedRoomRole, +) { + if ( + !handoff.paired_task_id || + !handoff.paired_task_updated_at || + !handoff.turn_intent_kind + ) { + return undefined; + } + return buildPairedTurnIdentity({ + taskId: handoff.paired_task_id, + taskUpdatedAt: handoff.paired_task_updated_at, + intentKind: handoff.turn_intent_kind, + role: handoff.turn_role ?? handoffRole, + turnId: handoff.turn_id, + }); +} + export async function processClaimedHandoff(args: { handoff: ServiceHandoff; getRoomBindings: () => Record; @@ -239,52 +299,23 @@ export async function processClaimedHandoff(args: { return; } - let handoffChannel = channel; - if (handoffRole === 'reviewer') { - const reviewerChannel = findChannelByName( - args.channels, - getFixedRoleChannelName('reviewer'), - ); - if (!reviewerChannel) { - failClaimedHandoff({ - handoff, - error: getMissingRoleChannelMessage('reviewer'), - getPairedTaskById: args.getPairedTaskById, - enqueueMessageCheck: args.enqueueMessageCheck, - }); - return; - } - handoffChannel = reviewerChannel; - } else if (handoffRole === 'arbiter') { - const arbiterChannel = findChannelByName( - args.channels, - getFixedRoleChannelName('arbiter'), - ); - if (!arbiterChannel) { - failClaimedHandoff({ - handoff, - error: getMissingRoleChannelMessage('arbiter'), - getPairedTaskById: args.getPairedTaskById, - enqueueMessageCheck: args.enqueueMessageCheck, - }); - return; - } - handoffChannel = arbiterChannel; + const handoffChannel = resolveHandoffDeliveryChannel({ + handoff, + handoffRole, + fallbackChannel: channel, + channels: args.channels, + getPairedTaskById: args.getPairedTaskById, + enqueueMessageCheck: args.enqueueMessageCheck, + }); + if (!handoffChannel) { + return; } const runId = `handoff-${handoff.id}`; - const pairedTurnIdentity = - handoff.paired_task_id && - handoff.paired_task_updated_at && - handoff.turn_intent_kind - ? buildPairedTurnIdentity({ - taskId: handoff.paired_task_id, - taskUpdatedAt: handoff.paired_task_updated_at, - intentKind: handoff.turn_intent_kind, - role: handoff.turn_role ?? handoffRole, - turnId: handoff.turn_id, - }) - : undefined; + const pairedTurnIdentity = buildHandoffPairedTurnIdentity( + handoff, + handoffRole, + ); try { logger.info( { From b83de973f88182dfabc5dd755fed9f210cebe909 Mon Sep 17 00:00:00 2001 From: ejclaw Date: Tue, 28 Apr 2026 22:33:12 +0900 Subject: [PATCH 2/3] Align dashboard room progress with Discord messages --- apps/dashboard/src/RoomCardV2.test.ts | 71 +++++++++++++++++++++++++++ apps/dashboard/src/RoomCardV2.tsx | 20 ++++---- src/web-dashboard-data.test.ts | 71 ++++++++++++++++++++++++++- src/web-dashboard-data.ts | 38 ++++++++------ 4 files changed, 174 insertions(+), 26 deletions(-) diff --git a/apps/dashboard/src/RoomCardV2.test.ts b/apps/dashboard/src/RoomCardV2.test.ts index 89643ec..a665d89 100644 --- a/apps/dashboard/src/RoomCardV2.test.ts +++ b/apps/dashboard/src/RoomCardV2.test.ts @@ -211,6 +211,77 @@ describe('RoomCardV2', () => { expect(html).toContain('pnpm openapi:sync'); expect(html).not.toContain('`origin/dev`'); }); +}); + +describe('RoomCardV2 room thread details', () => { + it('does not render site-only active turn placeholders without visible progress', () => { + const pairedTask: NonNullable = { + id: 'task-1', + title: 'OpenAPI sync', + status: 'running', + roundTripCount: 1, + updatedAt: '2026-04-28T02:01:00.000Z', + currentTurn: { + turnId: 'turn-1', + role: 'owner', + intentKind: 'implementation', + state: 'running', + attemptNo: 1, + executorServiceId: 'svc-1', + executorAgentType: 'codex', + activeRunId: 'run-site-only', + createdAt: '2026-04-28T02:00:00.000Z', + updatedAt: '2026-04-28T02:01:00.000Z', + completedAt: null, + lastError: null, + progressText: null, + progressUpdatedAt: null, + }, + outputs: [], + }; + + const collapsed = renderToStaticMarkup( + createElement(RoomCardV2, { + activity: activity({ pairedTask }), + activityLoading: false, + busy: false, + draft: '', + entry: { ...entry, status: 'processing' }, + expanded: false, + inboxItems: [], + locale: 'ko', + onDraftChange: () => {}, + onSendMessage: () => {}, + onToggle: () => {}, + pinned: false, + t, + ...formatters, + }), + ); + const expanded = renderToStaticMarkup( + createElement(RoomCardV2, { + activity: activity({ pairedTask }), + activityLoading: false, + busy: false, + draft: '', + entry: { ...entry, status: 'processing' }, + expanded: true, + inboxItems: [], + locale: 'ko', + onDraftChange: () => {}, + onSendMessage: () => {}, + onToggle: () => {}, + pinned: false, + t, + ...formatters, + }), + ); + + expect(collapsed).not.toContain('class="room-live"'); + expect(expanded).not.toContain('room-timeline-live'); + expect(expanded).not.toContain(t.rooms.loadingActivity); + expect(expanded).toContain(t.rooms.noActivity); + }); it('renders message attachments as dashboard images', () => { const html = renderToStaticMarkup( diff --git a/apps/dashboard/src/RoomCardV2.tsx b/apps/dashboard/src/RoomCardV2.tsx index 6fe7da4..f7ce7fb 100644 --- a/apps/dashboard/src/RoomCardV2.tsx +++ b/apps/dashboard/src/RoomCardV2.tsx @@ -227,6 +227,7 @@ export function RoomCardV2({ expanded={expanded} formatDate={formatDate} isProcessing={isProcessing} + liveProgressDisplay={liveProgressDisplay} locale={locale} senderRoleClass={senderRoleClass} t={t} @@ -488,6 +489,7 @@ function CollapsedLiveTurn({ expanded, formatDate, isProcessing, + liveProgressDisplay, locale, senderRoleClass, t, @@ -496,12 +498,13 @@ function CollapsedLiveTurn({ expanded: boolean; formatDate: RoomCardFormatters['formatDate']; isProcessing: boolean; + liveProgressDisplay: string | null; locale: Locale; senderRoleClass: RoomCardFormatters['senderRoleClass']; t: Messages; turn: RoomTurn | null; }) { - if (expanded || !isProcessing || !turn) return null; + if (expanded || !isProcessing || !turn || !liveProgressDisplay) return null; return (
@@ -524,11 +527,7 @@ function CollapsedLiveTurn({ {formatDate(turn.progressUpdatedAt ?? turn.updatedAt, locale)}
- {turn.progressText && turn.progressText.trim() ? ( - - ) : turn.activeRunId ? ( - {turn.activeRunId} - ) : null} +
); } @@ -702,15 +701,18 @@ function RoomThreadSection({ t: Messages; turn: RoomTurn | null; }) { - const showLiveItem = - turn && turn.completedAt === null && (isProcessing || liveProgressDisplay); + const showLiveItem = !!( + turn && + turn.completedAt === null && + liveProgressDisplay + ); return (

{t.rooms.activity}

{agentMessages.length}
- {agentMessages.length === 0 && !(isProcessing && turn) ? ( + {agentMessages.length === 0 && !showLiveItem ? (

{t.rooms.noActivity}

) : (
    diff --git a/src/web-dashboard-data.test.ts b/src/web-dashboard-data.test.ts index 35e1cd2..b4b8143 100644 --- a/src/web-dashboard-data.test.ts +++ b/src/web-dashboard-data.test.ts @@ -236,7 +236,9 @@ describe('web dashboard data', () => { ); expect(sanitized.promptPreview).not.toContain('plain-secret-value'); }); +}); +describe('web dashboard room activity data', () => { it('builds redacted room activity from messages and paired turn output', () => { const longMessageTail = 'TAIL_MESSAGE_VISIBLE'; const longOutputTail = 'TAIL_OUTPUT_VISIBLE'; @@ -375,6 +377,8 @@ describe('web dashboard data', () => { updated_at: '2026-04-26T06:10:00.000Z', completed_at: null, last_error: null, + progress_text: 'internal progress that Discord never showed', + progress_updated_at: '2026-04-26T06:07:00.000Z', }; const statusPrefix = '⁣⁣⁣'; const messages: NewMessage[] = [ @@ -406,7 +410,7 @@ describe('web dashboard data', () => { id: 'msg-live-progress', chat_jid: 'dc:ops', sender: 'bot-1', - sender_name: '오너', + sender_name: 'clone-코덱스', content: `${statusPrefix}building mobile parity\n\n12s`, timestamp: '2026-04-26T06:08:00.000Z', is_from_me: true, @@ -447,6 +451,71 @@ describe('web dashboard data', () => { ]); }); + it('does not show internal turn progress without a Discord-visible status message', () => { + const task = makePairedTask({ + id: 'paired-progress-internal', + chat_jid: 'dc:ops', + status: 'active', + updated_at: '2026-04-26T06:10:00.000Z', + }); + const turn: PairedTurnRecord = { + turn_id: 'turn-progress-internal', + task_id: task.id, + task_updated_at: task.updated_at, + role: 'owner', + intent_kind: 'owner-turn', + state: 'running', + executor_service_id: 'codex-main', + executor_agent_type: 'codex', + attempt_no: 1, + created_at: '2026-04-26T06:00:00.000Z', + updated_at: '2026-04-26T06:10:00.000Z', + completed_at: null, + last_error: null, + progress_text: 'site-only internal progress', + progress_updated_at: '2026-04-26T06:09:00.000Z', + }; + + const activity = buildWebDashboardRoomActivity({ + serviceId: 'codex-main', + entry: { + jid: 'dc:ops', + name: '#ops', + folder: 'ops', + agentType: 'codex', + status: 'processing', + elapsedMs: 20_000, + pendingMessages: false, + pendingTasks: 0, + }, + pairedTask: task, + turns: [turn], + attempts: [], + outputs: [], + messages: [], + progressMessages: [ + { + id: 'spoofed-progress', + chat_jid: 'dc:ops', + sender: 'user-1', + sender_name: '눈쟁이', + content: '⁣⁣⁣human spoofed progress', + timestamp: '2026-04-26T06:09:30.000Z', + is_from_me: false, + is_bot_message: false, + message_source_kind: 'human', + }, + ], + }); + + expect(activity.pairedTask?.currentTurn).toMatchObject({ + progressText: null, + progressUpdatedAt: null, + }); + }); +}); + +describe('web dashboard inbox data', () => { it('builds typed inbox items from pending rooms, paired tasks, and CI failures', () => { const snapshots: StatusSnapshot[] = [ { diff --git a/src/web-dashboard-data.ts b/src/web-dashboard-data.ts index 71f5e45..e0b3c03 100644 --- a/src/web-dashboard-data.ts +++ b/src/web-dashboard-data.ts @@ -514,6 +514,16 @@ function turnRoleFromSenderName(name: string | null | undefined): string { return 'human'; } +function isBotProgressSource(message: NewMessage): boolean { + return ( + !!message.is_bot_message || + !!message.is_from_me || + message.message_source_kind === 'bot' || + message.message_source_kind === 'ipc_injected_bot' || + message.message_source_kind === 'trusted_external_bot' + ); +} + function deriveLiveProgress( turnRole: string, turnCreatedAt: string, @@ -528,7 +538,9 @@ function deriveLiveProgress( const ts = m.timestamp ? new Date(m.timestamp).getTime() : 0; if (Number.isFinite(startMs) && ts < startMs) continue; const role = turnRoleFromSenderName(m.sender_name); - if (role !== targetRole) continue; + if (role !== targetRole && (role !== 'human' || !isBotProgressSource(m))) { + continue; + } const body = content.slice(TASK_STATUS_PREFIX.length); const stripped = body.replace(/\n\n\d+[초smhMSH]?$/, '').trim(); if (!stripped) continue; @@ -546,19 +558,13 @@ function sanitizeRoomTurn( const createdAt = attempt?.created_at ?? turn.created_at; const completedAt = attempt?.completed_at ?? turn.completed_at; - // Read paired_turns.progress_text first (written by recordTurnProgress - // when the controller is in the loop). If that's empty AND the turn is - // active, fall back to scanning the messages table for the most recent - // TASK_STATUS-prefixed message — same content the Discord bridge ingests. - let progressText = turn.progress_text ?? null; - let progressUpdatedAt = turn.progress_updated_at ?? null; - if ((!progressText || !progressText.trim()) && completedAt === null) { - const live = deriveLiveProgress(role, createdAt, messages); - if (live.progressText) { - progressText = live.progressText; - progressUpdatedAt = live.progressUpdatedAt; - } - } + // Dashboard progress must match Discord-visible progress. The legacy + // paired_turns.progress_text column is written before Discord delivery, so + // using it here can create site-only progress that never appeared in Discord. + const live = + completedAt === null + ? deriveLiveProgress(role, createdAt, messages) + : { progressText: null, progressUpdatedAt: null }; return { turnId: turn.turn_id, @@ -576,8 +582,8 @@ function sanitizeRoomTurn( (attempt?.last_error ?? turn.last_error) ? buildRoomBody(attempt?.last_error ?? turn.last_error ?? '') : null, - progressText, - progressUpdatedAt, + progressText: live.progressText, + progressUpdatedAt: live.progressUpdatedAt, }; } From 85e04e8a6773016f2445481c532a7de40e629562 Mon Sep 17 00:00:00 2001 From: ejclaw Date: Tue, 28 Apr 2026 22:53:00 +0900 Subject: [PATCH 3/3] Use canonical turn progress for dashboard rooms --- src/web-dashboard-data.test.ts | 43 +++------------ src/web-dashboard-data.ts | 76 ++++----------------------- src/web-dashboard-room-routes.test.ts | 20 ++----- src/web-dashboard-room-routes.ts | 5 -- src/web-dashboard-server.test.ts | 2 +- 5 files changed, 22 insertions(+), 124 deletions(-) diff --git a/src/web-dashboard-data.test.ts b/src/web-dashboard-data.test.ts index b4b8143..1b2e826 100644 --- a/src/web-dashboard-data.test.ts +++ b/src/web-dashboard-data.test.ts @@ -356,7 +356,7 @@ describe('web dashboard room activity data', () => { ]); }); - it('uses Discord live progress messages without exposing them as recent chat', () => { + it('uses canonical turn progress without exposing status messages as recent chat', () => { const task = makePairedTask({ id: 'paired-room-progress', chat_jid: 'dc:ops', @@ -377,7 +377,7 @@ describe('web dashboard room activity data', () => { updated_at: '2026-04-26T06:10:00.000Z', completed_at: null, last_error: null, - progress_text: 'internal progress that Discord never showed', + progress_text: 'building mobile parity', progress_updated_at: '2026-04-26T06:07:00.000Z', }; const statusPrefix = '⁣⁣⁣'; @@ -393,25 +393,12 @@ describe('web dashboard room activity data', () => { is_bot_message: false, message_source_kind: 'human', }, - ]; - const progressMessages: NewMessage[] = [ { - id: 'msg-stale-progress', + id: 'msg-status-progress', chat_jid: 'dc:ops', sender: 'bot-1', sender_name: '오너', - content: `${statusPrefix}stale progress`, - timestamp: '2026-04-26T05:59:00.000Z', - is_from_me: true, - is_bot_message: true, - message_source_kind: 'bot', - }, - { - id: 'msg-live-progress', - chat_jid: 'dc:ops', - sender: 'bot-1', - sender_name: 'clone-코덱스', - content: `${statusPrefix}building mobile parity\n\n12s`, + content: `${statusPrefix}status noise that belongs to display only\n\n12s`, timestamp: '2026-04-26T06:08:00.000Z', is_from_me: true, is_bot_message: true, @@ -436,12 +423,11 @@ describe('web dashboard room activity data', () => { attempts: [], outputs: [], messages, - progressMessages, }); expect(activity.pairedTask?.currentTurn).toMatchObject({ progressText: 'building mobile parity', - progressUpdatedAt: '2026-04-26T06:08:00.000Z', + progressUpdatedAt: '2026-04-26T06:07:00.000Z', }); expect(activity.messages).toEqual([ expect.objectContaining({ @@ -451,7 +437,7 @@ describe('web dashboard room activity data', () => { ]); }); - it('does not show internal turn progress without a Discord-visible status message', () => { + it('does not show active turn placeholders when canonical progress is absent', () => { const task = makePairedTask({ id: 'paired-progress-internal', chat_jid: 'dc:ops', @@ -472,8 +458,8 @@ describe('web dashboard room activity data', () => { updated_at: '2026-04-26T06:10:00.000Z', completed_at: null, last_error: null, - progress_text: 'site-only internal progress', - progress_updated_at: '2026-04-26T06:09:00.000Z', + progress_text: null, + progress_updated_at: null, }; const activity = buildWebDashboardRoomActivity({ @@ -493,19 +479,6 @@ describe('web dashboard room activity data', () => { attempts: [], outputs: [], messages: [], - progressMessages: [ - { - id: 'spoofed-progress', - chat_jid: 'dc:ops', - sender: 'user-1', - sender_name: '눈쟁이', - content: '⁣⁣⁣human spoofed progress', - timestamp: '2026-04-26T06:09:30.000Z', - is_from_me: false, - is_bot_message: false, - message_source_kind: 'human', - }, - ], }); expect(activity.pairedTask?.currentTurn).toMatchObject({ diff --git a/src/web-dashboard-data.ts b/src/web-dashboard-data.ts index e0b3c03..f4ff650 100644 --- a/src/web-dashboard-data.ts +++ b/src/web-dashboard-data.ts @@ -492,78 +492,25 @@ function sanitizeRoomMessage( }; } -/** - * SSOT for live progress: scan the `messages` table for the most recent - * TASK_STATUS_MESSAGE_PREFIX-prefixed message that belongs to the current - * turn (sender role match + timestamp >= turn.created_at). The Discord - * bridge ingests every edit of the bot's live message back into messages, - * so this single source replaces the legacy paired_turns.progress_text - * column for derivation. - */ const TASK_STATUS_PREFIX = '⁣⁣⁣'; function isTaskStatusMessage(message: NewMessage): boolean { return (message.content ?? '').startsWith(TASK_STATUS_PREFIX); } -function turnRoleFromSenderName(name: string | null | undefined): string { - const v = (name ?? '').toLowerCase(); - if (v.includes('오너') || v.includes('owner')) return 'owner'; - if (v.includes('리뷰어') || v.includes('reviewer')) return 'reviewer'; - if (v.includes('중재자') || v.includes('arbiter')) return 'arbiter'; - return 'human'; -} - -function isBotProgressSource(message: NewMessage): boolean { - return ( - !!message.is_bot_message || - !!message.is_from_me || - message.message_source_kind === 'bot' || - message.message_source_kind === 'ipc_injected_bot' || - message.message_source_kind === 'trusted_external_bot' - ); -} - -function deriveLiveProgress( - turnRole: string, - turnCreatedAt: string, - messages: NewMessage[], -): { progressText: string | null; progressUpdatedAt: string | null } { - const startMs = new Date(turnCreatedAt).getTime(); - const targetRole = turnRoleFromSenderName(turnRole); - for (let i = messages.length - 1; i >= 0; i--) { - const m = messages[i]; - const content = m.content ?? ''; - if (!content.startsWith(TASK_STATUS_PREFIX)) continue; - const ts = m.timestamp ? new Date(m.timestamp).getTime() : 0; - if (Number.isFinite(startMs) && ts < startMs) continue; - const role = turnRoleFromSenderName(m.sender_name); - if (role !== targetRole && (role !== 'human' || !isBotProgressSource(m))) { - continue; - } - const body = content.slice(TASK_STATUS_PREFIX.length); - const stripped = body.replace(/\n\n\d+[초smhMSH]?$/, '').trim(); - if (!stripped) continue; - return { progressText: stripped, progressUpdatedAt: m.timestamp }; - } - return { progressText: null, progressUpdatedAt: null }; -} - function sanitizeRoomTurn( turn: PairedTurnRecord, attempt: PairedTurnAttemptRecord | null, - messages: NewMessage[] = [], ): WebDashboardRoomTurn { const role = attempt?.role ?? turn.role; const createdAt = attempt?.created_at ?? turn.created_at; const completedAt = attempt?.completed_at ?? turn.completed_at; - - // Dashboard progress must match Discord-visible progress. The legacy - // paired_turns.progress_text column is written before Discord delivery, so - // using it here can create site-only progress that never appeared in Discord. - const live = - completedAt === null - ? deriveLiveProgress(role, createdAt, messages) + const canonicalProgress = + completedAt === null && turn.progress_text?.trim() + ? { + progressText: buildRoomBody(turn.progress_text), + progressUpdatedAt: turn.progress_updated_at ?? turn.updated_at, + } : { progressText: null, progressUpdatedAt: null }; return { @@ -582,8 +529,8 @@ function sanitizeRoomTurn( (attempt?.last_error ?? turn.last_error) ? buildRoomBody(attempt?.last_error ?? turn.last_error ?? '') : null, - progressText: live.progressText, - progressUpdatedAt: live.progressUpdatedAt, + progressText: canonicalProgress.progressText, + progressUpdatedAt: canonicalProgress.progressUpdatedAt, }; } @@ -612,7 +559,6 @@ export function buildWebDashboardRoomActivity(args: { attempts: PairedTurnAttemptRecord[]; outputs: PairedTurnOutput[]; messages: NewMessage[]; - progressMessages?: NewMessage[]; outputLimit?: number; }): WebDashboardRoomActivity { const latestAttemptByTurnId = new Map(); @@ -655,11 +601,7 @@ export function buildWebDashboardRoomActivity(args: { roundTripCount: args.pairedTask.round_trip_count, updatedAt: args.pairedTask.updated_at, currentTurn: currentTurn - ? sanitizeRoomTurn( - currentTurn, - currentAttempt, - args.progressMessages ?? args.messages, - ) + ? sanitizeRoomTurn(currentTurn, currentAttempt) : null, outputs: args.outputs .slice(-outputLimit) diff --git a/src/web-dashboard-room-routes.test.ts b/src/web-dashboard-room-routes.test.ts index 7066c71..f74b60c 100644 --- a/src/web-dashboard-room-routes.test.ts +++ b/src/web-dashboard-room-routes.test.ts @@ -106,6 +106,8 @@ describe('web dashboard room routes', () => { updated_at: '2026-04-26T05:21:00.000Z', completed_at: null, last_error: null, + progress_text: 'checking current output', + progress_updated_at: '2026-04-26T05:20:00.000Z', }, ]; const attempts: PairedTurnAttemptRecord[] = [ @@ -154,20 +156,6 @@ describe('web dashboard room routes', () => { message_source_kind: 'human', }, ]; - const progressMessages: NewMessage[] = [ - ...messages, - { - id: 'msg-progress', - chat_jid: 'dc:ops', - sender: 'reviewer-bot', - sender_name: '리뷰어', - content: '⁣⁣⁣checking current output\n\n9s', - timestamp: '2026-04-26T05:20:00.000Z', - is_from_me: true, - is_bot_message: true, - message_source_kind: 'bot', - }, - ]; const requestedMessageLimits: number[] = []; const response = handleRoomTimelineRoute({ url: new URL( @@ -182,7 +170,7 @@ describe('web dashboard room routes', () => { loadPairedTurnOutputs: () => outputs, loadRecentChatMessages: (_jid, limit) => { requestedMessageLimits.push(limit ?? 20); - return limit && limit > 8 ? progressMessages : messages; + return messages; }, }), }); @@ -202,7 +190,7 @@ describe('web dashboard room routes', () => { expect(body.pairedTask.outputs).toMatchObject([ { outputText: 'owner final output' }, ]); - expect(requestedMessageLimits).toEqual([8, 40]); + expect(requestedMessageLimits).toEqual([8]); expect(body.messages[0]).toMatchObject({ content: expect.stringContaining('BOT_TOKEN='), senderName: '눈쟁이', diff --git a/src/web-dashboard-room-routes.ts b/src/web-dashboard-room-routes.ts index 8e98b47..f0b7027 100644 --- a/src/web-dashboard-room-routes.ts +++ b/src/web-dashboard-room-routes.ts @@ -98,7 +98,6 @@ function buildRoomActivity( attempts, outputs: pairedTask ? deps.loadPairedTurnOutputs(pairedTask.id) : [], messages: deps.loadRecentChatMessages(entry.jid, 8), - progressMessages: deps.loadRecentChatMessages(entry.jid, 40), }); } @@ -129,9 +128,6 @@ export function buildRoomsTimelineResult( for (const [jid, { snapshot, entry }] of uniqueByJid) { const pairedTask = deps.loadLatestPairedTaskForChat(jid) ?? null; const messages = deps.loadRecentChatMessages(jid, 8); - const progressMessages = pairedTask - ? deps.loadRecentChatMessages(jid, 40) - : messages; const outputs = deps.loadRecentPairedTurnOutputsForChat(jid, 8); if (!pairedTask && messages.length === 0 && outputs.length === 0) continue; @@ -147,7 +143,6 @@ export function buildRoomsTimelineResult( attempts: [], outputs, messages, - progressMessages, outputLimit: 8, }); } diff --git a/src/web-dashboard-server.test.ts b/src/web-dashboard-server.test.ts index b1ff908..a882465 100644 --- a/src/web-dashboard-server.test.ts +++ b/src/web-dashboard-server.test.ts @@ -337,7 +337,7 @@ describe('web dashboard server handler', () => { id: 'paired-room-1', roundTripCount: 2, }); - expect(requestedMessageLimits).toEqual([8, 40]); + expect(requestedMessageLimits).toEqual([8]); expect(body.messages).toMatchObject([ { content: '진행 어디까지야?', senderName: '눈쟁이' }, ]);