fix: remove silent run budget

This commit is contained in:
Eyejoker
2026-03-23 04:07:12 +09:00
parent 562067352a
commit 3435b99cfc
2 changed files with 30 additions and 144 deletions

View File

@@ -896,7 +896,7 @@ describe('createMessageRuntime', () => {
}
});
it('retries a silent run internally before any visible output and succeeds in the same queue item', async () => {
it('does not retry or emit a synthetic final when a run completes silently', async () => {
const chatJid = 'group@test';
const group = makeGroup('codex');
const channel = makeChannel(chatJid);
@@ -915,25 +915,11 @@ describe('createMessageRuntime', () => {
},
]);
vi.mocked(agentRunner.runAgentProcess)
.mockResolvedValueOnce({
status: 'success',
result: null,
newSessionId: 'session-silent-rollover',
})
.mockImplementationOnce(async (_group, _input, _onProcess, onOutput) => {
await onOutput?.({
status: 'success',
phase: 'final',
result: '두 번째 시도에서 성공했습니다.',
newSessionId: 'session-silent-rollover',
});
return {
status: 'success',
result: null,
newSessionId: 'session-silent-rollover',
};
});
vi.mocked(agentRunner.runAgentProcess).mockResolvedValue({
status: 'success',
result: null,
newSessionId: 'session-silent-run',
});
const runtime = createMessageRuntime({
assistantName: 'Andy',
@@ -963,17 +949,14 @@ describe('createMessageRuntime', () => {
});
expect(result).toBe(true);
expect(agentRunner.runAgentProcess).toHaveBeenCalledTimes(2);
expect(agentRunner.runAgentProcess).toHaveBeenCalledTimes(1);
expect(saveState).toHaveBeenCalled();
expect(lastAgentTimestamps[chatJid]).toBe('1');
expect(channel.sendMessage).toHaveBeenCalledWith(
chatJid,
'두 번째 시도에서 성공했습니다.',
);
expect(channel.sendMessage).not.toHaveBeenCalled();
expect(channel.sendAndTrack).not.toHaveBeenCalled();
});
it('retries a silent run with the original prompt still included after a queued follow-up arrives', async () => {
it('does not emit a synthetic failure final when follow-up activity happens during a silent run', async () => {
const chatJid = 'group@test';
const group = makeGroup('codex');
const channel = makeChannel(chatJid);
@@ -1043,36 +1026,20 @@ describe('createMessageRuntime', () => {
},
);
vi.mocked(agentRunner.runAgentProcess)
.mockImplementationOnce(async () => {
lastAgentTimestamps[chatJid] = '2';
followUpPersisted = true;
activityTouch?.({
source: 'follow-up',
textLength: 9,
filename: 'follow-up.json',
});
return {
status: 'success',
result: null,
newSessionId: 'session-silent-follow-up',
};
})
.mockImplementationOnce(async (_group, input, _onProcess, onOutput) => {
expect(input.prompt).toContain('hello');
expect(input.prompt).toContain('follow-up');
await onOutput?.({
status: 'success',
phase: 'final',
result: '원래 요청과 follow-up을 같이 처리했습니다.',
newSessionId: 'session-silent-follow-up',
});
return {
status: 'success',
result: null,
newSessionId: 'session-silent-follow-up',
};
vi.mocked(agentRunner.runAgentProcess).mockImplementationOnce(async () => {
lastAgentTimestamps[chatJid] = '2';
followUpPersisted = true;
activityTouch?.({
source: 'follow-up',
textLength: 9,
filename: 'follow-up.json',
});
return {
status: 'success',
result: null,
newSessionId: 'session-silent-follow-up',
};
});
const runtime = createMessageRuntime({
assistantName: 'Andy',
@@ -1106,13 +1073,10 @@ describe('createMessageRuntime', () => {
});
expect(result).toBe(true);
expect(agentRunner.runAgentProcess).toHaveBeenCalledTimes(2);
expect(agentRunner.runAgentProcess).toHaveBeenCalledTimes(1);
expect(saveState).toHaveBeenCalled();
expect(lastAgentTimestamps[chatJid]).toBe('2');
expect(channel.sendMessage).toHaveBeenCalledWith(
chatJid,
'원래 요청과 follow-up을 같이 처리했습니다.',
);
expect(channel.sendMessage).not.toHaveBeenCalled();
});
it('resets tracked progress after a final output that becomes empty after formatting', async () => {
@@ -1549,12 +1513,11 @@ describe('createMessageRuntime', () => {
}
});
it('emits one visible failure final after silent rollovers exhaust the quiet budget', async () => {
it('does not emit a visible failure final when a run stays silent', async () => {
vi.useFakeTimers();
const chatJid = 'group@test';
const group = makeGroup('codex');
const channel = makeChannel(chatJid);
const closeStdin = vi.fn();
const lastAgentTimestamps: Record<string, string> = {};
const saveState = vi.fn();
@@ -1588,7 +1551,7 @@ describe('createMessageRuntime', () => {
channels: [channel],
queue: {
registerProcess: vi.fn(),
closeStdin,
closeStdin: vi.fn(),
notifyIdle: vi.fn(),
} as any,
getRegisteredGroups: () => ({ [chatJid]: group }),
@@ -1608,12 +1571,8 @@ describe('createMessageRuntime', () => {
});
expect(result).toBe(true);
expect(agentRunner.runAgentProcess).toHaveBeenCalledTimes(3);
expect(channel.sendMessage).toHaveBeenCalledTimes(1);
expect(channel.sendMessage).toHaveBeenCalledWith(
chatJid,
'요청을 완료하지 못했습니다. 다시 시도해 주세요.',
);
expect(agentRunner.runAgentProcess).toHaveBeenCalledTimes(1);
expect(channel.sendMessage).not.toHaveBeenCalled();
expect(lastAgentTimestamps[chatJid]).toBe('1');
expect(saveState).toHaveBeenCalled();
} finally {

View File

@@ -565,18 +565,8 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
const isMainGroup = group.isMain === true;
const isClaudeCodeAgent =
(group.agentType || 'claude-code') === 'claude-code';
const QUIET_RUN_ROLLOVER_MS = Math.min(deps.idleTimeout, 10_000);
const MAX_SILENT_ROLLOVERS = 2;
const MAX_TOTAL_SILENT_WAIT_MS =
QUIET_RUN_ROLLOVER_MS * (MAX_SILENT_ROLLOVERS + 1);
const FAILURE_FINAL_TEXT =
'요청을 완료하지 못했습니다. 다시 시도해 주세요.';
const queueItemBaseCursor = normalizeStoredSeqCursor(
deps.getLastAgentTimestamps()[chatJid] || '0',
chatJid,
);
let silentRolloverCount = 0;
let totalSilentWaitMs = 0;
while (true) {
const sinceSeqCursor = deps.getLastAgentTimestamps()[chatJid] || '0';
@@ -656,7 +646,6 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
}
const prompt = formatMessages(missedMessages, deps.timezone);
const previousCursor = deps.getLastAgentTimestamps()[chatJid] || '0';
const startSeq = missedMessages[0].seq ?? null;
const endSeq = missedMessages[missedMessages.length - 1].seq ?? null;
if (endSeq !== null) {
@@ -670,8 +659,6 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
groupFolder: group.folder,
runId,
messageCount: missedMessages.length,
silentRolloverCount,
totalSilentWaitMs,
},
'Dispatching queued messages to agent',
);
@@ -685,7 +672,6 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
let idleTimer: ReturnType<typeof setTimeout> | null = null;
let hadError = false;
let producedDeliverySucceeded = true;
let quietStopReason: string | null = null;
let visiblePhase: VisiblePhase = 'silent';
let latestProgressText: string | null = null;
let latestProgressRendered: string | null = null;
@@ -697,7 +683,6 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
let poisonedSessionDetected = false;
let canPipeFollowUps = true;
let pendingFollowUp: PendingFollowUp = null;
const attemptStartedAt = Date.now();
const hasVisibleOutput = () => visiblePhase !== 'silent';
const terminalObserved = () => visiblePhase === 'final';
@@ -935,10 +920,6 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
!hasVisibleOutput() && pendingFollowUp
? 'follow-up-no-output-preemption'
: 'idle-timeout';
quietStopReason ??=
!hasVisibleOutput() && pendingFollowUp
? 'follow-up-no-output-timeout'
: 'idle-timeout';
if (!hasVisibleOutput() && pendingFollowUp) {
logger.warn(
{
@@ -962,7 +943,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
}
deps.queue.closeStdin(chatJid, { runId, reason: closeReason });
},
hasVisibleOutput() ? deps.idleTimeout : QUIET_RUN_ROLLOVER_MS,
deps.idleTimeout,
);
};
@@ -1112,8 +1093,6 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
await finalizeProgressMessage();
latestProgressTextForFinal = null;
} else {
quietStopReason ??=
result.status === 'success' ? 'success-null-result' : 'error';
await finalizeProgressMessage();
}
@@ -1142,9 +1121,6 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
if (output === 'error') {
hadError = true;
quietStopReason ??= 'error';
} else if (visiblePhase === 'silent') {
quietStopReason ??= 'success-null-result';
}
const settledVisiblePhase = visiblePhase as VisiblePhase;
@@ -1169,64 +1145,17 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
} else if (
settledVisiblePhase === 'progress' &&
!terminalObserved() &&
(hadError || quietStopReason === 'idle-timeout')
hadError
) {
await publishFailureFinal();
}
const pendingFollowUpSnapshot = pendingFollowUp as PendingFollowUp;
const quietWaitStartedAt =
pendingFollowUpSnapshot?.queuedAt ?? attemptStartedAt;
const quietWaitMs = Date.now() - quietWaitStartedAt;
clearProgressTicker();
if (idleTimer) clearTimeout(idleTimer);
deps.queue.setActivityTouch?.(chatJid, null);
deps.queue.setFollowUpPipeAllowed?.(chatJid, null);
clearPendingFollowUp();
if (settledVisiblePhase === 'silent') {
const nextTotalSilentWaitMs = totalSilentWaitMs + quietWaitMs;
const canRolloverAgain =
silentRolloverCount < MAX_SILENT_ROLLOVERS &&
nextTotalSilentWaitMs <= MAX_TOTAL_SILENT_WAIT_MS;
if (canRolloverAgain) {
deps.getLastAgentTimestamps()[chatJid] = queueItemBaseCursor;
deps.saveState();
silentRolloverCount++;
totalSilentWaitMs = nextTotalSilentWaitMs;
logger.warn(
{
chatJid,
group: group.name,
groupFolder: group.folder,
runId,
quietStopReason,
rollbackCursor: queueItemBaseCursor,
silentRolloverCount,
totalSilentWaitMs,
},
'Retrying silent run within the same queue item',
);
continue;
}
logger.warn(
{
chatJid,
group: group.name,
groupFolder: group.folder,
runId,
quietStopReason,
silentRolloverCount,
totalSilentWaitMs: nextTotalSilentWaitMs,
},
'Silent run budget exhausted, emitting failure final',
);
await publishFailureFinal();
}
if (!producedDeliverySucceeded) {
logger.warn(
{ chatJid, group: group.name, groupFolder: group.folder, runId },
@@ -1242,8 +1171,6 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
groupFolder: group.folder,
runId,
visiblePhase: settledVisiblePhase,
silentRolloverCount,
totalSilentWaitMs,
},
'Queued run completed successfully',
);