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