From 29e39f25c9c3299c1b103eb76523ca95e548b233 Mon Sep 17 00:00:00 2001 From: ejclaw Date: Sat, 11 Apr 2026 09:56:59 +0900 Subject: [PATCH] paired: suppress reviewer progress flush before verdict --- src/message-turn-controller.test.ts | 47 ++++++++++++++++++++++++++++- src/message-turn-controller.ts | 19 ++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/message-turn-controller.test.ts b/src/message-turn-controller.test.ts index 64c0ba9..87eaada 100644 --- a/src/message-turn-controller.test.ts +++ b/src/message-turn-controller.test.ts @@ -117,10 +117,14 @@ describe('MessageTurnController outbound audit logging', () => { await controller.finish('success'); expect(channel.sendAndTrack).toHaveBeenCalledTimes(1); + expect(channel.sendAndTrack).toHaveBeenCalledWith( + 'dc:test-room', + expect.stringContaining('첫 진행 상황'), + ); expect(channel.editMessage).toHaveBeenCalledWith( 'dc:test-room', 'progress-1', - expect.stringContaining('둘째 진행 상황'), + expect.stringContaining('첫 진행 상황'), ); expect(deliverFinalText).toHaveBeenCalledWith('최종 답변'); @@ -274,6 +278,47 @@ describe('MessageTurnController outbound audit logging', () => { ); }); + it('does not flush pending progress before final delivery for paired reviewer turns', async () => { + const channel = makeChannel(); + const deliverFinalText = vi.fn().mockResolvedValue(true); + const controller = new MessageTurnController({ + chatJid: 'dc:test-room', + group: makeGroup(), + runId: 'run-review-no-pending-flush', + channel, + idleTimeout: 1_000, + failureFinalText: '실패', + isClaudeCodeAgent: true, + clearSession: vi.fn(), + requestClose: vi.fn(), + deliverFinalText, + deliveryRole: 'reviewer', + deliveryServiceId: 'codex-review', + pairedTurnIdentity: makeTurnIdentity(), + }); + + await controller.start(); + await controller.handleOutput({ + status: 'success', + phase: 'progress', + result: '오너가 대화와 관련 코드 근거를 대조해서 판정을 내리겠습니다.', + } as any); + + await controller.handleOutput({ + status: 'success', + phase: 'final', + result: 'PROCEED 근거를 확인했습니다.', + } as any); + await controller.finish('success'); + + expect(channel.sendAndTrack).not.toHaveBeenCalled(); + expect(channel.sendMessage).not.toHaveBeenCalled(); + expect(deliverFinalText).toHaveBeenCalledTimes(1); + expect(deliverFinalText).toHaveBeenCalledWith( + 'PROCEED 근거를 확인했습니다.', + ); + }); + it('suppresses replaying the last progress update as final when final delivery is disallowed', async () => { const channel = makeChannel(); const deliverFinalText = vi.fn().mockResolvedValue(true); diff --git a/src/message-turn-controller.ts b/src/message-turn-controller.ts index a149b00..8ad4902 100644 --- a/src/message-turn-controller.ts +++ b/src/message-turn-controller.ts @@ -505,6 +505,25 @@ export class MessageTurnController { * pending text differs from the final text. */ private async flushPendingProgress(finalText: string): Promise { + if ( + this.pendingProgressText && + (this.options.pairedTurnIdentity?.role === 'reviewer' || + this.options.pairedTurnIdentity?.role === 'arbiter') + ) { + this.log.info( + { + runId: this.options.runId, + deliveryRole: this.options.deliveryRole ?? null, + turnId: this.options.pairedTurnIdentity?.turnId ?? null, + pendingLength: this.pendingProgressText.length, + finalLength: finalText.length, + }, + 'Skipped flushing pending progress before final delivery for reviewer/arbiter turn', + ); + this.pendingProgressText = null; + return; + } + if (this.pendingProgressText && this.pendingProgressText !== finalText) { await this.sendProgressMessage(this.pendingProgressText); }