runtime: suppress stale owner progress updates
This commit is contained in:
@@ -461,7 +461,8 @@ describe('MessageTurnController outbound audit logging', () => {
|
|||||||
const finishResult = await controller.finish('success');
|
const finishResult = await controller.finish('success');
|
||||||
|
|
||||||
expect(finishResult.deliverySucceeded).toBe(true);
|
expect(finishResult.deliverySucceeded).toBe(true);
|
||||||
expect(channel.sendAndTrack).toHaveBeenCalledTimes(1);
|
expect(channel.sendAndTrack).not.toHaveBeenCalled();
|
||||||
|
expect(channel.sendMessage).not.toHaveBeenCalled();
|
||||||
expect(deliverFinalText).not.toHaveBeenCalled();
|
expect(deliverFinalText).not.toHaveBeenCalled();
|
||||||
expect(getAuditEntries()).not.toEqual(
|
expect(getAuditEntries()).not.toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
@@ -520,6 +521,122 @@ describe('MessageTurnController outbound audit logging', () => {
|
|||||||
expect(deliverFinalText).not.toHaveBeenCalled();
|
expect(deliverFinalText).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not emit a tracked progress message when a stale owner turn buffers multiple progress updates', async () => {
|
||||||
|
const channel = makeChannel();
|
||||||
|
const deliverFinalText = vi.fn().mockResolvedValue(true);
|
||||||
|
const controller = new MessageTurnController({
|
||||||
|
chatJid: 'dc:test-room',
|
||||||
|
group: makeGroup(),
|
||||||
|
runId: 'run-stale-owner-progress-buffer',
|
||||||
|
channel,
|
||||||
|
idleTimeout: 1_000,
|
||||||
|
failureFinalText: '실패',
|
||||||
|
isClaudeCodeAgent: true,
|
||||||
|
clearSession: vi.fn(),
|
||||||
|
requestClose: vi.fn(),
|
||||||
|
deliverFinalText,
|
||||||
|
canDeliverFinalText: () => false,
|
||||||
|
deliveryRole: 'owner',
|
||||||
|
pairedTurnIdentity: {
|
||||||
|
turnId: 'paired-task:2026-04-10T00:00:00.000Z:owner-turn',
|
||||||
|
taskId: 'paired-task',
|
||||||
|
taskUpdatedAt: '2026-04-10T00:00:00.000Z',
|
||||||
|
intentKind: 'owner-turn',
|
||||||
|
role: 'owner',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await controller.start();
|
||||||
|
await controller.handleOutput({
|
||||||
|
status: 'success',
|
||||||
|
phase: 'progress',
|
||||||
|
result: '첫 진행 상황',
|
||||||
|
output: { visibility: 'public', text: '첫 진행 상황' },
|
||||||
|
} as any);
|
||||||
|
await controller.handleOutput({
|
||||||
|
status: 'success',
|
||||||
|
phase: 'progress',
|
||||||
|
result: '둘째 진행 상황',
|
||||||
|
output: { visibility: 'public', text: '둘째 진행 상황' },
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
const finishResult = await controller.finish('success');
|
||||||
|
|
||||||
|
expect(finishResult.deliverySucceeded).toBe(true);
|
||||||
|
expect(channel.sendAndTrack).not.toHaveBeenCalled();
|
||||||
|
expect(channel.sendMessage).not.toHaveBeenCalled();
|
||||||
|
expect(deliverFinalText).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not edit an existing tracked progress message after a stale owner turn loses delivery ownership', async () => {
|
||||||
|
const channel = makeChannel();
|
||||||
|
const deliverFinalText = vi.fn().mockResolvedValue(true);
|
||||||
|
let canDeliver = true;
|
||||||
|
const controller = new MessageTurnController({
|
||||||
|
chatJid: 'dc:test-room',
|
||||||
|
group: makeGroup(),
|
||||||
|
runId: 'run-stale-owner-progress-edit',
|
||||||
|
channel,
|
||||||
|
idleTimeout: 1_000,
|
||||||
|
failureFinalText: '실패',
|
||||||
|
isClaudeCodeAgent: true,
|
||||||
|
clearSession: vi.fn(),
|
||||||
|
requestClose: vi.fn(),
|
||||||
|
deliverFinalText,
|
||||||
|
canDeliverFinalText: () => canDeliver,
|
||||||
|
deliveryRole: 'owner',
|
||||||
|
pairedTurnIdentity: {
|
||||||
|
turnId: 'paired-task:2026-04-10T00:00:00.000Z:owner-turn',
|
||||||
|
taskId: 'paired-task',
|
||||||
|
taskUpdatedAt: '2026-04-10T00:00:00.000Z',
|
||||||
|
intentKind: 'owner-turn',
|
||||||
|
role: 'owner',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await controller.start();
|
||||||
|
await controller.handleOutput({
|
||||||
|
status: 'success',
|
||||||
|
phase: 'progress',
|
||||||
|
result: '첫 진행 상황',
|
||||||
|
output: { visibility: 'public', text: '첫 진행 상황' },
|
||||||
|
} as any);
|
||||||
|
await controller.handleOutput({
|
||||||
|
status: 'success',
|
||||||
|
phase: 'progress',
|
||||||
|
result: '둘째 진행 상황',
|
||||||
|
output: { visibility: 'public', text: '둘째 진행 상황' },
|
||||||
|
} as any);
|
||||||
|
await flushAsync();
|
||||||
|
await controller.handleOutput({
|
||||||
|
status: 'success',
|
||||||
|
phase: 'progress',
|
||||||
|
result: '셋째 진행 상황',
|
||||||
|
output: { visibility: 'public', text: '셋째 진행 상황' },
|
||||||
|
} as any);
|
||||||
|
await flushAsync();
|
||||||
|
|
||||||
|
expect(channel.sendAndTrack).toHaveBeenCalledTimes(1);
|
||||||
|
expect(channel.editMessage).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
canDeliver = false;
|
||||||
|
await controller.handleOutput({
|
||||||
|
status: 'success',
|
||||||
|
phase: 'progress',
|
||||||
|
result: '넷째 진행 상황',
|
||||||
|
output: { visibility: 'public', text: '넷째 진행 상황' },
|
||||||
|
} as any);
|
||||||
|
await flushAsync();
|
||||||
|
|
||||||
|
const finishResult = await controller.finish('success');
|
||||||
|
|
||||||
|
expect(finishResult.deliverySucceeded).toBe(true);
|
||||||
|
expect(channel.sendAndTrack).toHaveBeenCalledTimes(1);
|
||||||
|
expect(channel.editMessage).toHaveBeenCalledTimes(1);
|
||||||
|
expect(channel.sendMessage).not.toHaveBeenCalled();
|
||||||
|
expect(deliverFinalText).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('replaces the tracked progress message when finish() publishes a failure final', async () => {
|
it('replaces the tracked progress message when finish() publishes a failure final', async () => {
|
||||||
const channel = {
|
const channel = {
|
||||||
...makeChannel(),
|
...makeChannel(),
|
||||||
|
|||||||
@@ -561,6 +561,23 @@ export class MessageTurnController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async syncTrackedProgressMessage(): Promise<void> {
|
private async syncTrackedProgressMessage(): Promise<void> {
|
||||||
|
if (
|
||||||
|
this.options.canDeliverFinalText &&
|
||||||
|
!this.options.canDeliverFinalText()
|
||||||
|
) {
|
||||||
|
this.log.info(
|
||||||
|
{
|
||||||
|
runId: this.options.runId,
|
||||||
|
deliveryRole: this.options.deliveryRole ?? null,
|
||||||
|
turnId: this.options.pairedTurnIdentity?.turnId ?? null,
|
||||||
|
progressMessageId: this.progressMessageId,
|
||||||
|
latestProgressLength: this.latestProgressText?.length ?? 0,
|
||||||
|
},
|
||||||
|
'Skipped editing tracked progress because this run no longer owns the active paired turn attempt',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!this.progressMessageId ||
|
!this.progressMessageId ||
|
||||||
!this.options.channel.editMessage ||
|
!this.options.channel.editMessage ||
|
||||||
@@ -713,6 +730,24 @@ export class MessageTurnController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async sendProgressMessage(text: string): Promise<void> {
|
private async sendProgressMessage(text: string): Promise<void> {
|
||||||
|
if (
|
||||||
|
this.options.canDeliverFinalText &&
|
||||||
|
!this.options.canDeliverFinalText()
|
||||||
|
) {
|
||||||
|
this.log.info(
|
||||||
|
{
|
||||||
|
runId: this.options.runId,
|
||||||
|
deliveryRole: this.options.deliveryRole ?? null,
|
||||||
|
turnId: this.options.pairedTurnIdentity?.turnId ?? null,
|
||||||
|
progressMessageId: this.progressMessageId,
|
||||||
|
textLength: text.length,
|
||||||
|
},
|
||||||
|
'Skipped progress delivery because this run no longer owns the active paired turn attempt',
|
||||||
|
);
|
||||||
|
this.pendingProgressText = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!text || (text === this.latestProgressText && this.progressMessageId)) {
|
if (!text || (text === this.latestProgressText && this.progressMessageId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user