From e863db61a87270eb8523333e4820d3c8ffef5b1c Mon Sep 17 00:00:00 2001 From: Eyejoker Date: Mon, 30 Mar 2026 03:22:08 +0900 Subject: [PATCH] refactor: remove dead output suppression path --- src/message-agent-executor.test.ts | 6 +- src/message-agent-executor.ts | 20 +- src/message-runtime.test.ts | 378 +-------------------------- src/message-runtime.ts | 12 +- src/message-turn-controller.ts | 14 +- src/output-suppression.test.ts | 140 ---------- src/output-suppression.ts | 33 --- src/paired-workspace-manager.test.ts | 58 ++-- 8 files changed, 33 insertions(+), 628 deletions(-) delete mode 100644 src/output-suppression.test.ts delete mode 100644 src/output-suppression.ts diff --git a/src/message-agent-executor.test.ts b/src/message-agent-executor.test.ts index 1367bb0..28f8159 100644 --- a/src/message-agent-executor.test.ts +++ b/src/message-agent-executor.test.ts @@ -224,7 +224,7 @@ describe('runAgentForGroup room memory', () => { ); }); - it('injects structured silent-output instructions into the agent prompt', async () => { + it('passes the prompt through unchanged', async () => { const group = { ...makeGroup(), folder: 'test-group' }; await runAgentForGroup(makeDeps(), { @@ -232,7 +232,6 @@ describe('runAgentForGroup room memory', () => { prompt: 'hello', chatJid: 'group@test', runId: 'run-suppress', - suppressToken: '__TEST_SUPPRESS__', }); expect(agentRunner.runAgentProcess).toHaveBeenCalledWith( @@ -273,7 +272,7 @@ describe('runAgentForGroup room memory', () => { ); }); - it('adds reviewer silence guidance when the current service is the reviewer for the chat', async () => { + it('keeps the reviewer prompt unchanged when the current service is the reviewer for the chat', async () => { const group = { ...makeGroup(), folder: 'test-group' }; vi.mocked(serviceRouting.getEffectiveChannelLease).mockReturnValue({ chat_jid: 'group@test', @@ -289,7 +288,6 @@ describe('runAgentForGroup room memory', () => { prompt: 'hello', chatJid: 'group@test', runId: 'run-review-suppress', - suppressToken: '__TEST_SUPPRESS__', }); expect(agentRunner.runAgentProcess).toHaveBeenCalledWith( diff --git a/src/message-agent-executor.ts b/src/message-agent-executor.ts index 430bb03..097f9d1 100644 --- a/src/message-agent-executor.ts +++ b/src/message-agent-executor.ts @@ -40,10 +40,6 @@ import { REVIEWER_AGENT_TYPE, isClaudeService, } from './config.js'; -import { - buildStructuredOutputPrompt, - classifySuppressTokenOutput, -} from './output-suppression.js'; import { activateCodexFailover, getEffectiveChannelLease, @@ -80,7 +76,6 @@ export async function runAgentForGroup( prompt: string; chatJid: string; runId: string; - suppressToken?: string; startSeq?: number | null; endSeq?: number | null; onOutput?: (output: AgentOutput) => Promise; @@ -91,7 +86,6 @@ export async function runAgentForGroup( prompt, chatJid, runId, - suppressToken, startSeq, endSeq, onOutput, @@ -174,12 +168,7 @@ export async function runAgentForGroup( runId, roomRoleContext, }); - const effectivePrompt = buildStructuredOutputPrompt(prompt, { - reviewerMode, - pairedRoom: !!pairedExecutionContext, - gateTurnKind: pairedExecutionContext?.gateTurnKind, - requiresVisibleVerdict: pairedExecutionContext?.requiresVisibleVerdict, - }); + const effectivePrompt = prompt; let pairedExecutionStatus: 'succeeded' | 'failed' = 'failed'; let pairedExecutionSummary: string | null = null; let pairedExecutionCompleted = false; @@ -399,14 +388,9 @@ export async function runAgentForGroup( if (!evaluation.shouldForwardOutput) { return; } - const suppressState = - typeof outputText === 'string' - ? classifySuppressTokenOutput(outputText, suppressToken) - : 'none'; if ( typeof outputText === 'string' && - outputText.length > 0 && - suppressState === 'none' + outputText.length > 0 ) { streamedState = { ...evaluation.state, diff --git a/src/message-runtime.test.ts b/src/message-runtime.test.ts index 823f044..955344a 100644 --- a/src/message-runtime.test.ts +++ b/src/message-runtime.test.ts @@ -11,15 +11,6 @@ vi.mock('./agent-runner.js', () => ({ writeTasksSnapshot: vi.fn(), })); -vi.mock('./output-suppression.js', async (importOriginal) => { - const actual = - await importOriginal(); - return { - ...actual, - createSuppressToken: vi.fn(() => '__TEST_SUPPRESS__'), - }; -}); - vi.mock('./config.js', () => ({ DATA_DIR: '/tmp/ejclaw-test-data', SERVICE_ID: 'claude', @@ -179,7 +170,6 @@ import * as agentRunner from './agent-runner.js'; import * as db from './db.js'; import { resolveGroupIpcPath } from './group-folder.js'; import { createMessageRuntime } from './message-runtime.js'; -import * as outputSuppression from './output-suppression.js'; import * as config from './config.js'; import type { Channel, RegisteredGroup } from './types.js'; @@ -345,7 +335,7 @@ describe('createMessageRuntime', () => { expect(saveState).toHaveBeenCalled(); }); - it('does not defer typing-on for suppress-capable review turns', async () => { + it('does not defer typing-on for review turns', async () => { const chatJid = 'group@test'; const group = makeGroup('codex'); const channel = makeChannel(chatJid); @@ -1374,65 +1364,6 @@ describe('createMessageRuntime', () => { expect(channel.sendAndTrack).not.toHaveBeenCalled(); }); - it('does not emit a visible message when the final output is the suppress token only', async () => { - const chatJid = 'group@test'; - const group = makeGroup('codex'); - const channel = makeChannel(chatJid); - const lastAgentTimestamps: Record = {}; - const saveState = vi.fn(); - - vi.mocked(db.getMessagesSince).mockReturnValue([ - { - id: 'msg-1', - chat_jid: chatJid, - sender: 'user@test', - sender_name: 'User', - content: 'hello', - timestamp: '2026-03-19T00:00:00.000Z', - seq: 1, - }, - ]); - - vi.mocked(agentRunner.runAgentProcess).mockResolvedValue({ - status: 'success', - result: '__TEST_SUPPRESS__', - newSessionId: 'session-suppress-run', - }); - - const runtime = createMessageRuntime({ - assistantName: 'Andy', - idleTimeout: 1_000, - pollInterval: 1_000, - timezone: 'UTC', - triggerPattern: /^@Andy\b/i, - channels: [channel], - queue: { - registerProcess: vi.fn(), - closeStdin: vi.fn(), - notifyIdle: vi.fn(), - } as any, - getRegisteredGroups: () => ({ [chatJid]: group }), - getSessions: () => ({}), - getLastTimestamp: () => '', - setLastTimestamp: vi.fn(), - getLastAgentTimestamps: () => lastAgentTimestamps, - saveState, - persistSession: vi.fn(), - clearSession: vi.fn(), - }); - - const result = await runtime.processGroupMessages(chatJid, { - runId: 'run-suppress-only', - reason: 'messages', - }); - - expect(result).toBe(true); - expect(saveState).toHaveBeenCalled(); - expect(lastAgentTimestamps[chatJid]).toBe('1'); - expect(channel.sendMessage).not.toHaveBeenCalled(); - expect(channel.sendAndTrack).not.toHaveBeenCalled(); - }); - it('does not emit a visible message when the final output is structured silent output', async () => { const chatJid = 'group@test'; const group = makeGroup('claude-code'); @@ -1587,7 +1518,7 @@ describe('createMessageRuntime', () => { } }); - it('starts typing immediately for suppress-capable turns with visible output', async () => { + it('starts typing immediately for turns with visible output', async () => { const chatJid = 'group@test'; const group = makeGroup('claude-code'); const channel = makeChannel(chatJid); @@ -1611,12 +1542,12 @@ describe('createMessageRuntime', () => { await onOutput?.({ status: 'success', phase: 'final', - result: 'visible suppress-capable reply', + result: 'visible reply', }); return { status: 'success', result: null, - newSessionId: 'session-visible-suppress-capable', + newSessionId: 'session-visible-reply', }; }, ); @@ -1644,316 +1575,19 @@ describe('createMessageRuntime', () => { }); const result = await runtime.processGroupMessages(chatJid, { - runId: 'run-visible-suppress-capable', + runId: 'run-visible-reply', reason: 'messages', }); expect(result).toBe(true); expect(channel.sendMessage).toHaveBeenCalledWith( chatJid, - 'visible suppress-capable reply', + 'visible reply', ); expect(channel.setTyping).toHaveBeenCalledWith(chatJid, true); expect(channel.setTyping).toHaveBeenCalledWith(chatJid, false); }); - it('does not grant suppress-token silence authority to codex-main', async () => { - const chatJid = 'group@test'; - const group = makeGroup('codex'); - const channel = makeChannel(chatJid); - const lastAgentTimestamps: Record = {}; - const saveState = vi.fn(); - - vi.mocked(config.isClaudeService).mockReturnValue(false); - vi.mocked(config.isReviewService).mockReturnValue(false); - vi.mocked(db.getMessagesSince).mockReturnValue([ - { - id: 'msg-1', - chat_jid: chatJid, - sender: 'user@test', - sender_name: 'User', - content: 'hello', - timestamp: '2026-03-19T00:00:00.000Z', - seq: 1, - }, - ]); - vi.mocked(agentRunner.runAgentProcess).mockResolvedValue({ - status: 'success', - result: 'visible codex reply', - newSessionId: 'session-codex-main', - }); - - const runtime = createMessageRuntime({ - assistantName: 'Andy', - idleTimeout: 1_000, - pollInterval: 1_000, - timezone: 'UTC', - triggerPattern: /^@Andy\b/i, - channels: [channel], - queue: { - registerProcess: vi.fn(), - closeStdin: vi.fn(), - notifyIdle: vi.fn(), - } as any, - getRegisteredGroups: () => ({ [chatJid]: group }), - getSessions: () => ({}), - getLastTimestamp: () => '', - setLastTimestamp: vi.fn(), - getLastAgentTimestamps: () => lastAgentTimestamps, - saveState, - persistSession: vi.fn(), - clearSession: vi.fn(), - }); - - const result = await runtime.processGroupMessages(chatJid, { - runId: 'run-codex-main-no-suppress', - reason: 'messages', - }); - - expect(result).toBe(true); - expect(outputSuppression.createSuppressToken).not.toHaveBeenCalled(); - }); - - it('blocks malformed output when the suppress token is mixed with visible text', async () => { - const chatJid = 'group@test'; - const group = makeGroup('codex'); - const channel = makeChannel(chatJid); - const lastAgentTimestamps: Record = {}; - const saveState = vi.fn(); - - vi.mocked(db.getMessagesSince).mockReturnValue([ - { - id: 'msg-1', - chat_jid: chatJid, - sender: 'user@test', - sender_name: 'User', - content: 'hello', - timestamp: '2026-03-19T00:00:00.000Z', - seq: 1, - }, - ]); - - vi.mocked(agentRunner.runAgentProcess).mockResolvedValue({ - status: 'success', - result: '동의합니다 __TEST_SUPPRESS__', - newSessionId: 'session-suppress-mixed', - }); - - const runtime = createMessageRuntime({ - assistantName: 'Andy', - idleTimeout: 1_000, - pollInterval: 1_000, - timezone: 'UTC', - triggerPattern: /^@Andy\b/i, - channels: [channel], - queue: { - registerProcess: vi.fn(), - closeStdin: vi.fn(), - notifyIdle: vi.fn(), - } as any, - getRegisteredGroups: () => ({ [chatJid]: group }), - getSessions: () => ({}), - getLastTimestamp: () => '', - setLastTimestamp: vi.fn(), - getLastAgentTimestamps: () => lastAgentTimestamps, - saveState, - persistSession: vi.fn(), - clearSession: vi.fn(), - }); - - const result = await runtime.processGroupMessages(chatJid, { - runId: 'run-suppress-mixed', - reason: 'messages', - }); - - expect(result).toBe(true); - expect(saveState).toHaveBeenCalled(); - expect(lastAgentTimestamps[chatJid]).toBe('1'); - expect(channel.sendMessage).not.toHaveBeenCalled(); - expect(channel.sendAndTrack).not.toHaveBeenCalled(); - }); - - it('suppresses a leaked foreign suppress token even when it differs from the current turn token', async () => { - const chatJid = 'group@test'; - const group = makeGroup('codex'); - const channel = makeChannel(chatJid); - const lastAgentTimestamps: Record = {}; - const saveState = vi.fn(); - - vi.mocked(config.isClaudeService).mockReturnValue(false); - vi.mocked(config.isReviewService).mockReturnValue(true); - vi.mocked(db.getMessagesSince).mockReturnValue([ - { - id: 'msg-1', - chat_jid: chatJid, - sender: 'user@test', - sender_name: 'User', - content: 'hello', - timestamp: '2026-03-19T00:00:00.000Z', - seq: 1, - }, - ]); - - vi.mocked(agentRunner.runAgentProcess).mockResolvedValue({ - status: 'success', - result: '__EJ_SUPPRESS_deadbeefdeadbeefdeadbeef__', - newSessionId: 'session-foreign-suppress-token', - }); - - const runtime = createMessageRuntime({ - assistantName: 'Andy', - idleTimeout: 1_000, - pollInterval: 1_000, - timezone: 'UTC', - triggerPattern: /^@Andy\b/i, - channels: [channel], - queue: { - registerProcess: vi.fn(), - closeStdin: vi.fn(), - notifyIdle: vi.fn(), - } as any, - getRegisteredGroups: () => ({ [chatJid]: group }), - getSessions: () => ({}), - getLastTimestamp: () => '', - setLastTimestamp: vi.fn(), - getLastAgentTimestamps: () => lastAgentTimestamps, - saveState, - persistSession: vi.fn(), - clearSession: vi.fn(), - }); - - const result = await runtime.processGroupMessages(chatJid, { - runId: 'run-foreign-suppress-token', - reason: 'messages', - }); - - expect(result).toBe(true); - expect(saveState).toHaveBeenCalled(); - expect(lastAgentTimestamps[chatJid]).toBe('1'); - expect(channel.sendMessage).not.toHaveBeenCalled(); - expect(channel.sendAndTrack).not.toHaveBeenCalled(); - }); - - it('suppresses a malformed leaked foreign suppress token without the closing suffix', async () => { - const chatJid = 'group@test'; - const group = makeGroup('codex'); - const channel = makeChannel(chatJid); - const lastAgentTimestamps: Record = {}; - const saveState = vi.fn(); - - vi.mocked(config.isClaudeService).mockReturnValue(false); - vi.mocked(config.isReviewService).mockReturnValue(true); - vi.mocked(db.getMessagesSince).mockReturnValue([ - { - id: 'msg-1', - chat_jid: chatJid, - sender: 'user@test', - sender_name: 'User', - content: 'hello', - timestamp: '2026-03-19T00:00:00.000Z', - seq: 1, - }, - ]); - - vi.mocked(agentRunner.runAgentProcess).mockResolvedValue({ - status: 'success', - result: '__EJ_SUPPRESS_deadbeefdeadbeefdeadbeef', - newSessionId: 'session-malformed-foreign-suppress-token', - }); - - const runtime = createMessageRuntime({ - assistantName: 'Andy', - idleTimeout: 1_000, - pollInterval: 1_000, - timezone: 'UTC', - triggerPattern: /^@Andy\b/i, - channels: [channel], - queue: { - registerProcess: vi.fn(), - closeStdin: vi.fn(), - notifyIdle: vi.fn(), - } as any, - getRegisteredGroups: () => ({ [chatJid]: group }), - getSessions: () => ({}), - getLastTimestamp: () => '', - setLastTimestamp: vi.fn(), - getLastAgentTimestamps: () => lastAgentTimestamps, - saveState, - persistSession: vi.fn(), - clearSession: vi.fn(), - }); - - const result = await runtime.processGroupMessages(chatJid, { - runId: 'run-malformed-foreign-suppress-token', - reason: 'messages', - }); - - expect(result).toBe(true); - expect(saveState).toHaveBeenCalled(); - expect(lastAgentTimestamps[chatJid]).toBe('1'); - expect(channel.sendMessage).not.toHaveBeenCalled(); - expect(channel.sendAndTrack).not.toHaveBeenCalled(); - }); - - it('suppresses a malformed structured silent envelope without sending a visible message', async () => { - const chatJid = 'group@test'; - const group = makeGroup('codex'); - const channel = makeChannel(chatJid); - const lastAgentTimestamps: Record = {}; - const saveState = vi.fn(); - - vi.mocked(config.isClaudeService).mockReturnValue(false); - vi.mocked(config.isReviewService).mockReturnValue(true); - vi.mocked(db.getMessagesSince).mockReturnValue([ - { - id: 'msg-1', - chat_jid: chatJid, - sender: 'user@test', - sender_name: 'User', - content: 'hello', - timestamp: '2026-03-19T00:00:00.000Z', - seq: 1, - }, - ]); - - vi.mocked(agentRunner.runAgentProcess).mockResolvedValue({ - status: 'success', - result: '{"ejclaw":{"visibility":"silent"}} extra', - newSessionId: 'session-malformed-structured-silent', - }); - - const runtime = createMessageRuntime({ - assistantName: 'Andy', - idleTimeout: 1_000, - pollInterval: 1_000, - timezone: 'UTC', - triggerPattern: /^@Andy\b/i, - channels: [channel], - queue: { - registerProcess: vi.fn(), - closeStdin: vi.fn(), - notifyIdle: vi.fn(), - } as any, - getRegisteredGroups: () => ({ [chatJid]: group }), - getSessions: () => ({}), - getLastTimestamp: () => '', - setLastTimestamp: vi.fn(), - getLastAgentTimestamps: () => lastAgentTimestamps, - saveState, - persistSession: vi.fn(), - clearSession: vi.fn(), - }); - - const result = await runtime.processGroupMessages(chatJid, { - runId: 'run-malformed-structured-silent', - reason: 'messages', - }); - - expect(result).toBe(true); - expect(channel.sendMessage).not.toHaveBeenCalled(); - expect(channel.sendAndTrack).not.toHaveBeenCalled(); - }); - it('resets tracked progress after a final output that becomes empty after formatting', async () => { vi.useFakeTimers(); const chatJid = 'group@test'; diff --git a/src/message-runtime.ts b/src/message-runtime.ts index 4684468..c2434be 100644 --- a/src/message-runtime.ts +++ b/src/message-runtime.ts @@ -20,8 +20,6 @@ import { type WorkItem, } from './db.js'; import { - isClaudeService, - isReviewService, isSessionCommandSenderAllowed, REVIEWER_AGENT_TYPE, SERVICE_AGENT_TYPE, @@ -45,7 +43,6 @@ import { } from './message-runtime-rules.js'; import { runAgentForGroup } from './message-agent-executor.js'; import { MessageTurnController } from './message-turn-controller.js'; -import { createSuppressToken } from './output-suppression.js'; import { extractSessionCommand, handleSessionCommand, @@ -232,7 +229,6 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): { runId: string, onOutput?: (output: AgentOutput) => Promise, options?: { - suppressToken?: string; startSeq?: number | null; endSeq?: number | null; }, @@ -251,7 +247,6 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): { prompt, chatJid, runId, - suppressToken: options?.suppressToken, startSeq: options?.startSeq, endSeq: options?.endSeq, onOutput, @@ -280,10 +275,6 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): { const { group, prompt, chatJid, runId, channel, startSeq, endSeq } = args; const isClaudeCodeAgent = (group.agentType || 'claude-code') === 'claude-code'; - const suppressToken = - isClaudeService() || isReviewService() - ? createSuppressToken() - : undefined; const turnController = new MessageTurnController({ chatJid, @@ -293,7 +284,6 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): { idleTimeout: deps.idleTimeout, failureFinalText: FAILURE_FINAL_TEXT, isClaudeCodeAgent, - suppressToken, clearSession: () => deps.clearSession(group.folder), requestClose: (reason) => deps.queue.closeStdin(chatJid, { runId, reason }), @@ -327,7 +317,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): { chatJid, runId, (result) => turnController.handleOutput(result), - { suppressToken, startSeq, endSeq }, + { startSeq, endSeq }, ); const { deliverySucceeded, visiblePhase } = diff --git a/src/message-turn-controller.ts b/src/message-turn-controller.ts index 105799c..6a74717 100644 --- a/src/message-turn-controller.ts +++ b/src/message-turn-controller.ts @@ -1,7 +1,6 @@ import { type AgentOutput } from './agent-runner.js'; -import { getAgentOutputText, isSilentAgentOutput } from './agent-output.js'; +import { getAgentOutputText } from './agent-output.js'; import { logger } from './logger.js'; -import { classifySuppressTokenOutput } from './output-suppression.js'; import { formatOutbound } from './router.js'; import { shouldResetSessionOnAgentFailure } from './session-recovery.js'; import { TASK_STATUS_MESSAGE_PREFIX } from './task-watch-status.js'; @@ -30,7 +29,6 @@ interface MessageTurnControllerOptions { idleTimeout: number; failureFinalText: string; isClaudeCodeAgent: boolean; - suppressToken?: string; clearSession: () => void; requestClose: (reason: string) => void; deliverFinalText: (text: string) => Promise; @@ -122,8 +120,6 @@ export class MessageTurnController { } const raw = getAgentOutputText(result); - const silentOutput = false; - const suppressState = 'none' as const; const text = raw ? formatOutbound(raw) : null; if (raw) { @@ -267,14 +263,6 @@ export class MessageTurnController { await this.flushPendingProgress(text); await this.finalizeProgressMessage(); await this.deliverFinalText(text); - } else if (silentOutput || suppressState !== 'none') { - const shouldPromoteVisibleProgressToFinal = - this.visiblePhase === 'progress' && - typeof this.latestProgressTextForFinal === 'string'; - await this.finalizeProgressMessage(); - if (!shouldPromoteVisibleProgressToFinal) { - this.latestProgressTextForFinal = null; - } } else if (raw) { logger.info( { diff --git a/src/output-suppression.test.ts b/src/output-suppression.test.ts deleted file mode 100644 index 076c7f4..0000000 --- a/src/output-suppression.test.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { - buildStructuredOutputPrompt, - classifySuppressTokenOutput, - parseStructuredOutputEnvelope, -} from './output-suppression.js'; - -describe('classifySuppressTokenOutput', () => { - it('treats the current turn suppress token as exact', () => { - expect( - classifySuppressTokenOutput( - '__EJ_SUPPRESS_deadbeefdeadbeefdeadbeef__', - '__EJ_SUPPRESS_deadbeefdeadbeefdeadbeef__', - ), - ).toBe('exact'); - }); - - it('treats a leaked foreign suppress token as exact silent output', () => { - expect( - classifySuppressTokenOutput( - '__EJ_SUPPRESS_feedfacefeedfacefeedface__', - '__EJ_SUPPRESS_deadbeefdeadbeefdeadbeef__', - ), - ).toBe('exact'); - }); - - it('treats a malformed foreign suppress token without the closing suffix as exact silent output', () => { - expect( - classifySuppressTokenOutput( - '__EJ_SUPPRESS_feedfacefeedfacefeedface', - '__EJ_SUPPRESS_deadbeefdeadbeefdeadbeef__', - ), - ).toBe('exact'); - }); - - it('treats a suppress token embedded in visible text as mixed', () => { - expect( - classifySuppressTokenOutput( - 'prefix __EJ_SUPPRESS_feedfacefeedfacefeedface__ suffix', - '__EJ_SUPPRESS_deadbeefdeadbeefdeadbeef__', - ), - ).toBe('mixed'); - }); - - it('treats the exact structured silent envelope as exact silent output', () => { - expect( - classifySuppressTokenOutput( - '{"ejclaw":{"visibility":"silent"}}', - undefined, - ), - ).toBe('exact'); - }); - - it('treats a truncated structured silent envelope as mixed', () => { - expect( - classifySuppressTokenOutput( - '{"ejclaw":{"visibility":"silent"', - undefined, - ), - ).toBe('mixed'); - }); - - it('treats a structured silent envelope mixed with extra text as mixed', () => { - expect( - classifySuppressTokenOutput( - '{"ejclaw":{"visibility":"silent"}} extra', - undefined, - ), - ).toBe('mixed'); - }); -}); - -describe('parseStructuredOutputEnvelope', () => { - it('parses the exact silent envelope', () => { - expect( - parseStructuredOutputEnvelope('{"ejclaw":{"visibility":"silent"}}'), - ).toEqual({ visibility: 'silent' }); - }); - - it('parses a public envelope', () => { - expect( - parseStructuredOutputEnvelope( - '{"ejclaw":{"visibility":"public","text":"hello"}}', - ), - ).toEqual({ visibility: 'public', text: 'hello' }); - }); - - it('parses a public envelope with a reviewer verdict', () => { - expect( - parseStructuredOutputEnvelope( - '{"ejclaw":{"visibility":"public","verdict":"done_with_concerns","text":"**DONE_WITH_CONCERNS**"}}', - ), - ).toEqual({ - visibility: 'public', - verdict: 'done_with_concerns', - text: '**DONE_WITH_CONCERNS**', - }); - }); -}); - -describe('buildStructuredOutputPrompt', () => { - it('prepends the structured output control block', () => { - expect(buildStructuredOutputPrompt('hello')).toContain( - 'If you have no user-visible content to send for this turn, output exactly this JSON and nothing else: {"ejclaw":{"visibility":"silent"}}', - ); - expect(buildStructuredOutputPrompt('hello')).toContain( - 'If you have already emitted any visible progress, status update, or partial answer earlier in this turn, do not end with the JSON object. Finish with a short visible final conclusion for the user instead.', - ); - }); - - it('tightens the reviewer silent rule when reviewer mode is enabled', () => { - expect( - buildStructuredOutputPrompt('hello', { reviewerMode: true }), - ).toContain( - 'If you have not already emitted any visible progress, status update, or partial answer in this turn and you are only agreeing, mirroring, or restating without adding a concrete correction, risk, missing prerequisite, test gap, or code change, output only the JSON object.', - ); - }); - - it('requires a visible structured verdict on reviewer gate turns', () => { - expect( - buildStructuredOutputPrompt('hello', { - reviewerMode: true, - gateTurnKind: 'implementation_start', - requiresVisibleVerdict: true, - }), - ).toContain( - 'This turn is a paired-room gate turn for implementation_start. Silent output is forbidden.', - ); - expect( - buildStructuredOutputPrompt('hello', { - reviewerMode: true, - gateTurnKind: 'implementation_start', - requiresVisibleVerdict: true, - }), - ).toContain( - 'Allowed verdict values are: "done", "done_with_concerns", "blocked".', - ); - }); -}); diff --git a/src/output-suppression.ts b/src/output-suppression.ts deleted file mode 100644 index 6c02edc..0000000 --- a/src/output-suppression.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Output suppression was removed — harness-level protections (isOwnMessage filter, - * shouldSkipBotOnlyCollaboration, PAIRED_MAX_ROUND_TRIPS) now prevent infinite loops. - * - * Retained exports are stubs so callers compile without changes. - */ - -export function createSuppressToken(): string | undefined { - return undefined; -} - -export function classifySuppressTokenOutput( - _rawText: string, - _suppressToken: string | undefined, -): 'none' { - return 'none'; -} - -export function buildStructuredOutputPrompt( - prompt: string, - _options?: { - reviewerMode?: boolean; - pairedRoom?: boolean; - gateTurnKind?: string | null; - requiresVisibleVerdict?: boolean; - }, -): string { - return prompt; -} - -export function parseStructuredOutputEnvelope(_rawText: string): null { - return null; -} diff --git a/src/paired-workspace-manager.test.ts b/src/paired-workspace-manager.test.ts index 771abbd..e167688 100644 --- a/src/paired-workspace-manager.test.ts +++ b/src/paired-workspace-manager.test.ts @@ -45,7 +45,7 @@ describe('paired workspace manager', () => { fs.rmSync(tempRoot, { recursive: true, force: true }); }); - it('provisions an owner worktree and refreshes a reviewer shadow snapshot', async () => { + it('registers the owner workspace for reviewer execution when review is requested', async () => { const { db, manager } = await loadModules(); db._initTestDatabase(); @@ -113,15 +113,10 @@ describe('paired workspace manager', () => { 'utf-8', ), ).toBe('review me\n'); - expect( - runGit( - ['config', '--local', '--get', 'remote.origin.pushurl'], - reviewerWorkspace.workspace_dir, - ), - ).toBe('DISABLED_BY_EJCLAW'); - expect( - runGit(['status', '--short'], reviewerWorkspace.workspace_dir), - ).toContain('M README.md'); + expect(reviewerWorkspace.workspace_dir).toBe(ownerWorkspace.workspace_dir); + expect(reviewerWorkspace.snapshot_source_dir).toBe( + ownerWorkspace.workspace_dir, + ); expect(db.getPairedTaskById('paired-task-1')?.status).toBe('review_ready'); expect( db.getPairedTaskById('paired-task-1')?.review_requested_at, @@ -129,9 +124,9 @@ describe('paired workspace manager', () => { expect( db.getPairedWorkspace('paired-task-1', 'reviewer')?.snapshot_refreshed_at, ).toBeTruthy(); - expect( - db.getPairedWorkspace('paired-task-1', 'reviewer')?.snapshot_ref, - ).toBeTruthy(); + expect(db.getPairedWorkspace('paired-task-1', 'reviewer')?.snapshot_ref).toBe( + null, + ); }); it('uses the shared DB owner workspace across service-local data dirs', async () => { @@ -493,7 +488,7 @@ describe('paired workspace manager', () => { ).toBe('EXAMPLE=1\n'); }); - it('auto-refreshes a missing reviewer snapshot after an explicit review request', async () => { + it('registers a reviewer workspace when an explicit review request already exists', async () => { const { db, manager } = await loadModules(); db._initTestDatabase(); @@ -541,8 +536,12 @@ describe('paired workspace manager', () => { db.getPairedTaskById('paired-task-5b')!, ); - expect(result.autoRefreshed).toBe(true); + expect(result.autoRefreshed).toBe(false); expect(result.blockMessage).toBeUndefined(); + expect(result.workspace?.workspace_dir).toBe(ownerWorkspace.workspace_dir); + expect(result.workspace?.snapshot_source_dir).toBe( + ownerWorkspace.workspace_dir, + ); expect( fs.readFileSync( path.join(result.workspace!.workspace_dir, 'README.md'), @@ -555,7 +554,7 @@ describe('paired workspace manager', () => { ); }); - it('blocks once when an in-review snapshot becomes stale, then refreshes on retry', async () => { + it('reuses the live owner workspace during in-review turns', async () => { const { db, manager } = await loadModules(); db._initTestDatabase(); @@ -609,37 +608,22 @@ describe('paired workspace manager', () => { 'owner changed again\n', ); - const blocked = manager.prepareReviewerWorkspaceForExecution( + const result = manager.prepareReviewerWorkspaceForExecution( db.getPairedTaskById('paired-task-6')!, ); - expect(blocked.workspace).toBeNull(); - expect(blocked.autoRefreshed).toBe(false); - expect(blocked.blockMessage).toBe( - 'Review snapshot is stale after owner changes. Retry the review once to refresh against the latest owner workspace.', - ); - expect(db.getPairedTaskById('paired-task-6')?.status).toBe('review_ready'); + expect(result.workspace?.workspace_dir).toBe(ownerWorkspace.workspace_dir); + expect(result.autoRefreshed).toBe(false); + expect(result.blockMessage).toBeUndefined(); + expect(db.getPairedTaskById('paired-task-6')?.status).toBe('in_review'); expect( db.getPairedTaskById('paired-task-6')?.review_requested_at, ).toBeTruthy(); - expect(db.getPairedWorkspace('paired-task-6', 'reviewer')?.status).toBe( - 'stale', - ); - - const refreshed = manager.prepareReviewerWorkspaceForExecution( - db.getPairedTaskById('paired-task-6')!, - ); - - expect(refreshed.autoRefreshed).toBe(true); - expect(refreshed.blockMessage).toBeUndefined(); expect( fs.readFileSync( - path.join(refreshed.workspace!.workspace_dir, 'README.md'), + path.join(result.workspace!.workspace_dir, 'README.md'), 'utf-8', ), ).toBe('owner changed again\n'); - expect( - db.getPairedTaskById('paired-task-6')?.review_requested_at, - ).toBeTruthy(); }); });