fix: suppress paired completion status reentry
This commit is contained in:
@@ -138,4 +138,41 @@ describe('filterProcessableMessages', () => {
|
||||
expect(result[0].id).toBe('human-1');
|
||||
expect(result[1].id).toBe('other-1');
|
||||
});
|
||||
|
||||
it('filters paired execution status control bot messages in paired rooms', () => {
|
||||
const isOwn = (m: NewMessage) =>
|
||||
m.is_bot_message === true && m.sender === OWN_BOT_ID;
|
||||
const result = filterProcessableMessages(
|
||||
[
|
||||
makeMsg({ id: 'human-1', content: 'human' }),
|
||||
makeMsg({
|
||||
id: 'control-1',
|
||||
sender: 'other-bot-456',
|
||||
sender_name: 'OtherBot',
|
||||
content: '<@216851709744513024> ✅ 작업 완료.',
|
||||
is_bot_message: true,
|
||||
}),
|
||||
makeMsg({
|
||||
id: 'control-2',
|
||||
sender: 'other-bot-456',
|
||||
sender_name: 'OtherBot',
|
||||
content: '⚠️ 자동 해결 불가 — 확인이 필요합니다.',
|
||||
is_bot_message: true,
|
||||
}),
|
||||
makeMsg({
|
||||
id: 'other-1',
|
||||
sender: 'other-bot-456',
|
||||
sender_name: 'OtherBot',
|
||||
content: 'partner response',
|
||||
is_bot_message: true,
|
||||
}),
|
||||
],
|
||||
true,
|
||||
isOwn,
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].id).toBe('human-1');
|
||||
expect(result[1].id).toBe('other-1');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { isSessionCommandControlMessage } from './session-commands.js';
|
||||
import { NewMessage } from './types.js';
|
||||
|
||||
const PAIRED_EXECUTION_CONTROL_MESSAGE_PATTERN =
|
||||
/^(?:<@\d+>\s+)?(?:✅ 작업 완료\.|⚠️ 자동 해결 불가 — 확인이 필요합니다\.|⚠️ 중재자 판단: 사람 개입이 필요합니다\.)$/;
|
||||
|
||||
function isPairedExecutionControlMessage(content: string): boolean {
|
||||
return PAIRED_EXECUTION_CONTROL_MESSAGE_PATTERN.test(content.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter messages before processing.
|
||||
* - Normal rooms: drop all bot messages.
|
||||
@@ -16,7 +23,8 @@ export function filterProcessableMessages(
|
||||
(message) =>
|
||||
!(
|
||||
message.is_bot_message &&
|
||||
isSessionCommandControlMessage(message.content)
|
||||
(isSessionCommandControlMessage(message.content) ||
|
||||
isPairedExecutionControlMessage(message.content))
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -244,7 +244,6 @@ export function createPairedExecutionLifecycle(args: {
|
||||
const sender = getLastHumanMessageSender(chatJid);
|
||||
const mention = sender ? `<@${sender}>` : '';
|
||||
const notifications: Record<string, string> = {
|
||||
done: `${mention} ✅ 작업 완료.`,
|
||||
escalated: `${mention} ⚠️ 자동 해결 불가 — 확인이 필요합니다.`,
|
||||
arbiter_escalated: `${mention} ⚠️ 중재자 판단: 사람 개입이 필요합니다.`,
|
||||
};
|
||||
|
||||
@@ -837,6 +837,176 @@ describe('runAgentForGroup room memory', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('does not emit an extra public done notification after owner finalization completes the paired task', async () => {
|
||||
const group = {
|
||||
...makeGroup(),
|
||||
folder: 'test-group',
|
||||
workDir: '/repo/canonical',
|
||||
};
|
||||
const outputs: string[] = [];
|
||||
|
||||
vi.mocked(db.getLastHumanMessageSender).mockReturnValue('216851709744513024');
|
||||
vi.mocked(
|
||||
pairedExecutionContext.preparePairedExecutionContext,
|
||||
).mockReturnValue({
|
||||
task: {
|
||||
id: 'paired-task-final-done',
|
||||
chat_jid: 'group@test',
|
||||
group_folder: 'test-group',
|
||||
owner_service_id: 'claude',
|
||||
reviewer_service_id: 'codex-main',
|
||||
title: null,
|
||||
source_ref: 'HEAD',
|
||||
plan_notes: null,
|
||||
round_trip_count: 1,
|
||||
review_requested_at: '2026-04-09T00:00:00.000Z',
|
||||
status: 'merge_ready',
|
||||
arbiter_verdict: null,
|
||||
arbiter_requested_at: null,
|
||||
completion_reason: null,
|
||||
created_at: '2026-04-09T00:00:00.000Z',
|
||||
updated_at: '2026-04-09T00:00:00.000Z',
|
||||
},
|
||||
workspace: null,
|
||||
envOverrides: {},
|
||||
});
|
||||
vi.mocked(db.getPairedTaskById).mockReturnValue({
|
||||
id: 'paired-task-final-done',
|
||||
chat_jid: 'group@test',
|
||||
group_folder: 'test-group',
|
||||
owner_service_id: 'claude',
|
||||
reviewer_service_id: 'codex-main',
|
||||
title: null,
|
||||
source_ref: 'HEAD',
|
||||
plan_notes: null,
|
||||
round_trip_count: 1,
|
||||
review_requested_at: '2026-04-09T00:00:00.000Z',
|
||||
status: 'completed',
|
||||
arbiter_verdict: null,
|
||||
arbiter_requested_at: null,
|
||||
completion_reason: 'done',
|
||||
created_at: '2026-04-09T00:00:00.000Z',
|
||||
updated_at: '2026-04-09T00:00:01.000Z',
|
||||
});
|
||||
vi.mocked(agentRunner.runAgentProcess).mockImplementation(
|
||||
async (_group, _input, _onProcess, onOutput) => {
|
||||
await onOutput?.({
|
||||
status: 'success',
|
||||
result: 'DONE\nfinalized',
|
||||
output: { visibility: 'public', text: 'DONE\nfinalized' },
|
||||
phase: 'final',
|
||||
});
|
||||
return {
|
||||
status: 'success',
|
||||
result: 'DONE\nfinalized',
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const result = await runAgentForGroup(makeDeps(), {
|
||||
group,
|
||||
prompt: 'finalize',
|
||||
chatJid: 'group@test',
|
||||
runId: 'run-final-done',
|
||||
onOutput: async (output) => {
|
||||
if (output.output?.visibility === 'public' && output.output.text) {
|
||||
outputs.push(output.output.text);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe('success');
|
||||
expect(outputs).toEqual(['DONE\nfinalized']);
|
||||
});
|
||||
|
||||
it('still emits escalation notifications when the paired task completes with human intervention required', async () => {
|
||||
const group = {
|
||||
...makeGroup(),
|
||||
folder: 'test-group',
|
||||
workDir: '/repo/canonical',
|
||||
};
|
||||
const outputs: string[] = [];
|
||||
|
||||
vi.mocked(db.getLastHumanMessageSender).mockReturnValue('216851709744513024');
|
||||
vi.mocked(
|
||||
pairedExecutionContext.preparePairedExecutionContext,
|
||||
).mockReturnValue({
|
||||
task: {
|
||||
id: 'paired-task-final-escalated',
|
||||
chat_jid: 'group@test',
|
||||
group_folder: 'test-group',
|
||||
owner_service_id: 'claude',
|
||||
reviewer_service_id: 'codex-main',
|
||||
title: null,
|
||||
source_ref: 'HEAD',
|
||||
plan_notes: null,
|
||||
round_trip_count: 1,
|
||||
review_requested_at: '2026-04-09T00:00:00.000Z',
|
||||
status: 'merge_ready',
|
||||
arbiter_verdict: null,
|
||||
arbiter_requested_at: null,
|
||||
completion_reason: null,
|
||||
created_at: '2026-04-09T00:00:00.000Z',
|
||||
updated_at: '2026-04-09T00:00:00.000Z',
|
||||
},
|
||||
workspace: null,
|
||||
envOverrides: {},
|
||||
});
|
||||
vi.mocked(db.getPairedTaskById).mockReturnValue({
|
||||
id: 'paired-task-final-escalated',
|
||||
chat_jid: 'group@test',
|
||||
group_folder: 'test-group',
|
||||
owner_service_id: 'claude',
|
||||
reviewer_service_id: 'codex-main',
|
||||
title: null,
|
||||
source_ref: 'HEAD',
|
||||
plan_notes: null,
|
||||
round_trip_count: 1,
|
||||
review_requested_at: '2026-04-09T00:00:00.000Z',
|
||||
status: 'completed',
|
||||
arbiter_verdict: null,
|
||||
arbiter_requested_at: null,
|
||||
completion_reason: 'escalated',
|
||||
created_at: '2026-04-09T00:00:00.000Z',
|
||||
updated_at: '2026-04-09T00:00:01.000Z',
|
||||
});
|
||||
vi.mocked(agentRunner.runAgentProcess).mockImplementation(
|
||||
async (_group, _input, _onProcess, onOutput) => {
|
||||
await onOutput?.({
|
||||
status: 'success',
|
||||
result: 'BLOCKED\nhuman intervention required',
|
||||
output: {
|
||||
visibility: 'public',
|
||||
text: 'BLOCKED\nhuman intervention required',
|
||||
},
|
||||
phase: 'final',
|
||||
});
|
||||
return {
|
||||
status: 'success',
|
||||
result: 'BLOCKED\nhuman intervention required',
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const result = await runAgentForGroup(makeDeps(), {
|
||||
group,
|
||||
prompt: 'finalize',
|
||||
chatJid: 'group@test',
|
||||
runId: 'run-final-escalated',
|
||||
onOutput: async (output) => {
|
||||
if (output.output?.visibility === 'public' && output.output.text) {
|
||||
outputs.push(output.output.text);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe('success');
|
||||
expect(outputs).toEqual([
|
||||
'BLOCKED\nhuman intervention required',
|
||||
'<@216851709744513024> ⚠️ 자동 해결 불가 — 확인이 필요합니다.',
|
||||
]);
|
||||
});
|
||||
|
||||
it('logs streamed activity with resolved execution attribution', async () => {
|
||||
const group = {
|
||||
...makeGroup(),
|
||||
|
||||
Reference in New Issue
Block a user