fix: throttle progress edits via 5s ticker, prevent Discord rate limit
- Remove per-tool-activity sync calls (was flooding Discord with edits) - Ticker-only sync at 5s intervals for time + tool activity updates - Summary updates heading directly when progress message exists - Prevent duplicate progress message creation with progressCreating flag
This commit is contained in:
@@ -1451,14 +1451,15 @@ describe('createMessageRuntime', () => {
|
||||
result: '커밋은 정상 들어갔고 pre-commit도 통과했습니다.',
|
||||
newSessionId: 'session-progress-only',
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(10_000);
|
||||
// Third progress: flushes second (edits tracked message with new text)
|
||||
// Third progress: updates tracked message heading directly
|
||||
await onOutput?.({
|
||||
status: 'success',
|
||||
phase: 'progress',
|
||||
result: '테스트도 통과했습니다.',
|
||||
newSessionId: 'session-progress-only',
|
||||
});
|
||||
// Advance timer so the ticker fires and syncs the tracked message
|
||||
await vi.advanceTimersByTimeAsync(5_000);
|
||||
await onOutput?.({
|
||||
status: 'success',
|
||||
result: null,
|
||||
@@ -1506,17 +1507,17 @@ describe('createMessageRuntime', () => {
|
||||
chatJid,
|
||||
P('검증 중입니다.\n\n0초'),
|
||||
);
|
||||
// Second progress flushed when third arrives — edits tracked message
|
||||
// Ticker fires after advanceTimersByTime — edits tracked message with latest heading
|
||||
expect(channel.editMessage).toHaveBeenCalledWith(
|
||||
chatJid,
|
||||
'progress-1',
|
||||
P('커밋은 정상 들어갔고 pre-commit도 통과했습니다.\n\n10초'),
|
||||
P('테스트도 통과했습니다.\n\n5초'),
|
||||
);
|
||||
// finish() promotes the last flushed progress text to a final message
|
||||
expect(channel.sendMessage).toHaveBeenCalledTimes(1);
|
||||
expect(channel.sendMessage).toHaveBeenCalledWith(
|
||||
chatJid,
|
||||
'커밋은 정상 들어갔고 pre-commit도 통과했습니다.',
|
||||
'검증 중입니다.',
|
||||
);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
@@ -1561,14 +1562,15 @@ describe('createMessageRuntime', () => {
|
||||
result: '아직 진행 중.',
|
||||
newSessionId: 'session-progress-recreate',
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(10_000);
|
||||
// Third progress: flushes second (edit fails once, then succeeds on retry in syncTrackedProgressMessage)
|
||||
// Third progress: updates heading directly (edit fails once on ticker, then succeeds on retry)
|
||||
await onOutput?.({
|
||||
status: 'success',
|
||||
phase: 'progress',
|
||||
result: '거의 완료.',
|
||||
newSessionId: 'session-progress-recreate',
|
||||
});
|
||||
// Advance timer so the ticker fires and syncs (first edit fails, second succeeds)
|
||||
await vi.advanceTimersByTimeAsync(5_000);
|
||||
await onOutput?.({
|
||||
status: 'success',
|
||||
result: null,
|
||||
@@ -1627,7 +1629,7 @@ describe('createMessageRuntime', () => {
|
||||
expect(channel.sendMessage).toHaveBeenCalledTimes(1);
|
||||
expect(channel.sendMessage).toHaveBeenCalledWith(
|
||||
chatJid,
|
||||
'아직 진행 중.',
|
||||
'진행 중입니다.',
|
||||
);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
|
||||
@@ -29,6 +29,7 @@ export class MessageTurnController {
|
||||
private previousProgressText: string | null = null;
|
||||
private pendingProgressText: string | null = null;
|
||||
private toolActivities: string[] = [];
|
||||
private progressCreating = false;
|
||||
private latestProgressRendered: string | null = null;
|
||||
private progressMessageId: string | null = null;
|
||||
private progressStartedAt: number | null = null;
|
||||
@@ -105,9 +106,18 @@ export class MessageTurnController {
|
||||
}
|
||||
|
||||
if (result.phase === 'tool-activity') {
|
||||
// Flush pending progress so the message exists for tool activity sub-lines
|
||||
if (this.pendingProgressText && !this.progressMessageId) {
|
||||
void this.sendProgressMessage(this.pendingProgressText);
|
||||
// Ensure a progress message exists for tool activity sub-lines
|
||||
if (!this.progressMessageId && !this.progressCreating) {
|
||||
this.progressCreating = true;
|
||||
const heading = this.pendingProgressText || '작업 중...';
|
||||
this.sendProgressMessage(heading).then(() => {
|
||||
this.progressCreating = false;
|
||||
this.ensureProgressTicker();
|
||||
// Replay any queued tool activities now that the message exists
|
||||
if (this.toolActivities.length > 0 && this.progressMessageId) {
|
||||
void this.syncTrackedProgressMessage();
|
||||
}
|
||||
});
|
||||
this.pendingProgressText = null;
|
||||
}
|
||||
if (text) {
|
||||
@@ -121,7 +131,14 @@ export class MessageTurnController {
|
||||
|
||||
if (result.phase === 'progress') {
|
||||
if (text) {
|
||||
this.bufferProgress(text);
|
||||
if (this.progressMessageId) {
|
||||
// Progress message already visible — update heading directly
|
||||
this.latestProgressText = text;
|
||||
this.toolActivities = [];
|
||||
void this.syncTrackedProgressMessage();
|
||||
} else {
|
||||
this.bufferProgress(text);
|
||||
}
|
||||
}
|
||||
if (!this.poisonedSessionDetected) {
|
||||
this.resetIdleTimer();
|
||||
@@ -223,7 +240,7 @@ export class MessageTurnController {
|
||||
const elapsedSeconds =
|
||||
this.progressStartedAt === null
|
||||
? 0
|
||||
: Math.floor((Date.now() - this.progressStartedAt) / 10_000) * 10;
|
||||
: Math.floor((Date.now() - this.progressStartedAt) / 5_000) * 5;
|
||||
const hours = Math.floor(elapsedSeconds / 3600);
|
||||
const minutes = Math.floor((elapsedSeconds % 3600) / 60);
|
||||
const seconds = elapsedSeconds % 60;
|
||||
@@ -241,9 +258,7 @@ export class MessageTurnController {
|
||||
const isLast = i === this.toolActivities.length - 1;
|
||||
const connector = isLast ? '└' : '├';
|
||||
const isSummary = a.startsWith('📋');
|
||||
return isSummary
|
||||
? `${connector} ${a}`
|
||||
: `${connector} ${a}`;
|
||||
return isSummary ? `${connector} ${a}` : `${connector} ${a}`;
|
||||
})
|
||||
.join('\n')
|
||||
: '';
|
||||
@@ -267,6 +282,7 @@ export class MessageTurnController {
|
||||
private resetProgressState(): void {
|
||||
this.clearProgressTicker();
|
||||
this.pendingProgressText = null;
|
||||
this.progressCreating = false;
|
||||
this.toolActivities = [];
|
||||
this.latestProgressText = null;
|
||||
this.previousProgressText = null;
|
||||
@@ -299,11 +315,9 @@ export class MessageTurnController {
|
||||
if (this.toolActivities.length > MAX_ACTIVITIES) {
|
||||
this.toolActivities = this.toolActivities.slice(-MAX_ACTIVITIES);
|
||||
}
|
||||
// Update the displayed progress message with tool activity sub-lines
|
||||
if (this.latestProgressText && this.progressMessageId) {
|
||||
void this.syncTrackedProgressMessage();
|
||||
this.ensureProgressTicker();
|
||||
}
|
||||
// Don't sync here — let the ticker handle periodic updates
|
||||
// to avoid flooding Discord with edits.
|
||||
this.ensureProgressTicker();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -327,9 +341,6 @@ export class MessageTurnController {
|
||||
}
|
||||
|
||||
const rendered = this.renderProgressMessage(this.latestProgressText);
|
||||
if (rendered === this.latestProgressRendered) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.options.channel.editMessage(
|
||||
@@ -361,17 +372,15 @@ export class MessageTurnController {
|
||||
}
|
||||
|
||||
private ensureProgressTicker(): void {
|
||||
if (
|
||||
!this.progressMessageId ||
|
||||
!this.options.channel.editMessage ||
|
||||
this.progressTicker
|
||||
) {
|
||||
if (this.progressTicker || !this.options.channel.editMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.progressTicker = setInterval(() => {
|
||||
void this.syncTrackedProgressMessage();
|
||||
}, 10_000);
|
||||
if (this.progressMessageId && this.latestProgressText && !this.progressCreating) {
|
||||
void this.syncTrackedProgressMessage();
|
||||
}
|
||||
}, 5_000);
|
||||
}
|
||||
|
||||
private async finalizeProgressMessage(): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user