fix: remove silent run budget
This commit is contained in:
@@ -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 chatJid = 'group@test';
|
||||||
const group = makeGroup('codex');
|
const group = makeGroup('codex');
|
||||||
const channel = makeChannel(chatJid);
|
const channel = makeChannel(chatJid);
|
||||||
@@ -915,25 +915,11 @@ describe('createMessageRuntime', () => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
vi.mocked(agentRunner.runAgentProcess)
|
vi.mocked(agentRunner.runAgentProcess).mockResolvedValue({
|
||||||
.mockResolvedValueOnce({
|
status: 'success',
|
||||||
status: 'success',
|
result: null,
|
||||||
result: null,
|
newSessionId: 'session-silent-run',
|
||||||
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',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const runtime = createMessageRuntime({
|
const runtime = createMessageRuntime({
|
||||||
assistantName: 'Andy',
|
assistantName: 'Andy',
|
||||||
@@ -963,17 +949,14 @@ describe('createMessageRuntime', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
expect(agentRunner.runAgentProcess).toHaveBeenCalledTimes(2);
|
expect(agentRunner.runAgentProcess).toHaveBeenCalledTimes(1);
|
||||||
expect(saveState).toHaveBeenCalled();
|
expect(saveState).toHaveBeenCalled();
|
||||||
expect(lastAgentTimestamps[chatJid]).toBe('1');
|
expect(lastAgentTimestamps[chatJid]).toBe('1');
|
||||||
expect(channel.sendMessage).toHaveBeenCalledWith(
|
expect(channel.sendMessage).not.toHaveBeenCalled();
|
||||||
chatJid,
|
|
||||||
'두 번째 시도에서 성공했습니다.',
|
|
||||||
);
|
|
||||||
expect(channel.sendAndTrack).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 chatJid = 'group@test';
|
||||||
const group = makeGroup('codex');
|
const group = makeGroup('codex');
|
||||||
const channel = makeChannel(chatJid);
|
const channel = makeChannel(chatJid);
|
||||||
@@ -1043,36 +1026,20 @@ describe('createMessageRuntime', () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
vi.mocked(agentRunner.runAgentProcess)
|
vi.mocked(agentRunner.runAgentProcess).mockImplementationOnce(async () => {
|
||||||
.mockImplementationOnce(async () => {
|
lastAgentTimestamps[chatJid] = '2';
|
||||||
lastAgentTimestamps[chatJid] = '2';
|
followUpPersisted = true;
|
||||||
followUpPersisted = true;
|
activityTouch?.({
|
||||||
activityTouch?.({
|
source: 'follow-up',
|
||||||
source: 'follow-up',
|
textLength: 9,
|
||||||
textLength: 9,
|
filename: 'follow-up.json',
|
||||||
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',
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
return {
|
||||||
|
status: 'success',
|
||||||
|
result: null,
|
||||||
|
newSessionId: 'session-silent-follow-up',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const runtime = createMessageRuntime({
|
const runtime = createMessageRuntime({
|
||||||
assistantName: 'Andy',
|
assistantName: 'Andy',
|
||||||
@@ -1106,13 +1073,10 @@ describe('createMessageRuntime', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
expect(agentRunner.runAgentProcess).toHaveBeenCalledTimes(2);
|
expect(agentRunner.runAgentProcess).toHaveBeenCalledTimes(1);
|
||||||
expect(saveState).toHaveBeenCalled();
|
expect(saveState).toHaveBeenCalled();
|
||||||
expect(lastAgentTimestamps[chatJid]).toBe('2');
|
expect(lastAgentTimestamps[chatJid]).toBe('2');
|
||||||
expect(channel.sendMessage).toHaveBeenCalledWith(
|
expect(channel.sendMessage).not.toHaveBeenCalled();
|
||||||
chatJid,
|
|
||||||
'원래 요청과 follow-up을 같이 처리했습니다.',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('resets tracked progress after a final output that becomes empty after formatting', async () => {
|
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();
|
vi.useFakeTimers();
|
||||||
const chatJid = 'group@test';
|
const chatJid = 'group@test';
|
||||||
const group = makeGroup('codex');
|
const group = makeGroup('codex');
|
||||||
const channel = makeChannel(chatJid);
|
const channel = makeChannel(chatJid);
|
||||||
const closeStdin = vi.fn();
|
|
||||||
const lastAgentTimestamps: Record<string, string> = {};
|
const lastAgentTimestamps: Record<string, string> = {};
|
||||||
const saveState = vi.fn();
|
const saveState = vi.fn();
|
||||||
|
|
||||||
@@ -1588,7 +1551,7 @@ describe('createMessageRuntime', () => {
|
|||||||
channels: [channel],
|
channels: [channel],
|
||||||
queue: {
|
queue: {
|
||||||
registerProcess: vi.fn(),
|
registerProcess: vi.fn(),
|
||||||
closeStdin,
|
closeStdin: vi.fn(),
|
||||||
notifyIdle: vi.fn(),
|
notifyIdle: vi.fn(),
|
||||||
} as any,
|
} as any,
|
||||||
getRegisteredGroups: () => ({ [chatJid]: group }),
|
getRegisteredGroups: () => ({ [chatJid]: group }),
|
||||||
@@ -1608,12 +1571,8 @@ describe('createMessageRuntime', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
expect(agentRunner.runAgentProcess).toHaveBeenCalledTimes(3);
|
expect(agentRunner.runAgentProcess).toHaveBeenCalledTimes(1);
|
||||||
expect(channel.sendMessage).toHaveBeenCalledTimes(1);
|
expect(channel.sendMessage).not.toHaveBeenCalled();
|
||||||
expect(channel.sendMessage).toHaveBeenCalledWith(
|
|
||||||
chatJid,
|
|
||||||
'요청을 완료하지 못했습니다. 다시 시도해 주세요.',
|
|
||||||
);
|
|
||||||
expect(lastAgentTimestamps[chatJid]).toBe('1');
|
expect(lastAgentTimestamps[chatJid]).toBe('1');
|
||||||
expect(saveState).toHaveBeenCalled();
|
expect(saveState).toHaveBeenCalled();
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -565,18 +565,8 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
const isMainGroup = group.isMain === true;
|
const isMainGroup = group.isMain === true;
|
||||||
const isClaudeCodeAgent =
|
const isClaudeCodeAgent =
|
||||||
(group.agentType || 'claude-code') === 'claude-code';
|
(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 FAILURE_FINAL_TEXT =
|
||||||
'요청을 완료하지 못했습니다. 다시 시도해 주세요.';
|
'요청을 완료하지 못했습니다. 다시 시도해 주세요.';
|
||||||
const queueItemBaseCursor = normalizeStoredSeqCursor(
|
|
||||||
deps.getLastAgentTimestamps()[chatJid] || '0',
|
|
||||||
chatJid,
|
|
||||||
);
|
|
||||||
let silentRolloverCount = 0;
|
|
||||||
let totalSilentWaitMs = 0;
|
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const sinceSeqCursor = deps.getLastAgentTimestamps()[chatJid] || '0';
|
const sinceSeqCursor = deps.getLastAgentTimestamps()[chatJid] || '0';
|
||||||
@@ -656,7 +646,6 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const prompt = formatMessages(missedMessages, deps.timezone);
|
const prompt = formatMessages(missedMessages, deps.timezone);
|
||||||
const previousCursor = deps.getLastAgentTimestamps()[chatJid] || '0';
|
|
||||||
const startSeq = missedMessages[0].seq ?? null;
|
const startSeq = missedMessages[0].seq ?? null;
|
||||||
const endSeq = missedMessages[missedMessages.length - 1].seq ?? null;
|
const endSeq = missedMessages[missedMessages.length - 1].seq ?? null;
|
||||||
if (endSeq !== null) {
|
if (endSeq !== null) {
|
||||||
@@ -670,8 +659,6 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
groupFolder: group.folder,
|
groupFolder: group.folder,
|
||||||
runId,
|
runId,
|
||||||
messageCount: missedMessages.length,
|
messageCount: missedMessages.length,
|
||||||
silentRolloverCount,
|
|
||||||
totalSilentWaitMs,
|
|
||||||
},
|
},
|
||||||
'Dispatching queued messages to agent',
|
'Dispatching queued messages to agent',
|
||||||
);
|
);
|
||||||
@@ -685,7 +672,6 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
let idleTimer: ReturnType<typeof setTimeout> | null = null;
|
let idleTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
let hadError = false;
|
let hadError = false;
|
||||||
let producedDeliverySucceeded = true;
|
let producedDeliverySucceeded = true;
|
||||||
let quietStopReason: string | null = null;
|
|
||||||
let visiblePhase: VisiblePhase = 'silent';
|
let visiblePhase: VisiblePhase = 'silent';
|
||||||
let latestProgressText: string | null = null;
|
let latestProgressText: string | null = null;
|
||||||
let latestProgressRendered: string | null = null;
|
let latestProgressRendered: string | null = null;
|
||||||
@@ -697,7 +683,6 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
let poisonedSessionDetected = false;
|
let poisonedSessionDetected = false;
|
||||||
let canPipeFollowUps = true;
|
let canPipeFollowUps = true;
|
||||||
let pendingFollowUp: PendingFollowUp = null;
|
let pendingFollowUp: PendingFollowUp = null;
|
||||||
const attemptStartedAt = Date.now();
|
|
||||||
|
|
||||||
const hasVisibleOutput = () => visiblePhase !== 'silent';
|
const hasVisibleOutput = () => visiblePhase !== 'silent';
|
||||||
const terminalObserved = () => visiblePhase === 'final';
|
const terminalObserved = () => visiblePhase === 'final';
|
||||||
@@ -935,10 +920,6 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
!hasVisibleOutput() && pendingFollowUp
|
!hasVisibleOutput() && pendingFollowUp
|
||||||
? 'follow-up-no-output-preemption'
|
? 'follow-up-no-output-preemption'
|
||||||
: 'idle-timeout';
|
: 'idle-timeout';
|
||||||
quietStopReason ??=
|
|
||||||
!hasVisibleOutput() && pendingFollowUp
|
|
||||||
? 'follow-up-no-output-timeout'
|
|
||||||
: 'idle-timeout';
|
|
||||||
if (!hasVisibleOutput() && pendingFollowUp) {
|
if (!hasVisibleOutput() && pendingFollowUp) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
{
|
{
|
||||||
@@ -962,7 +943,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
}
|
}
|
||||||
deps.queue.closeStdin(chatJid, { runId, reason: closeReason });
|
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();
|
await finalizeProgressMessage();
|
||||||
latestProgressTextForFinal = null;
|
latestProgressTextForFinal = null;
|
||||||
} else {
|
} else {
|
||||||
quietStopReason ??=
|
|
||||||
result.status === 'success' ? 'success-null-result' : 'error';
|
|
||||||
await finalizeProgressMessage();
|
await finalizeProgressMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1142,9 +1121,6 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
|
|
||||||
if (output === 'error') {
|
if (output === 'error') {
|
||||||
hadError = true;
|
hadError = true;
|
||||||
quietStopReason ??= 'error';
|
|
||||||
} else if (visiblePhase === 'silent') {
|
|
||||||
quietStopReason ??= 'success-null-result';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const settledVisiblePhase = visiblePhase as VisiblePhase;
|
const settledVisiblePhase = visiblePhase as VisiblePhase;
|
||||||
@@ -1169,64 +1145,17 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
} else if (
|
} else if (
|
||||||
settledVisiblePhase === 'progress' &&
|
settledVisiblePhase === 'progress' &&
|
||||||
!terminalObserved() &&
|
!terminalObserved() &&
|
||||||
(hadError || quietStopReason === 'idle-timeout')
|
hadError
|
||||||
) {
|
) {
|
||||||
await publishFailureFinal();
|
await publishFailureFinal();
|
||||||
}
|
}
|
||||||
|
|
||||||
const pendingFollowUpSnapshot = pendingFollowUp as PendingFollowUp;
|
|
||||||
const quietWaitStartedAt =
|
|
||||||
pendingFollowUpSnapshot?.queuedAt ?? attemptStartedAt;
|
|
||||||
const quietWaitMs = Date.now() - quietWaitStartedAt;
|
|
||||||
|
|
||||||
clearProgressTicker();
|
clearProgressTicker();
|
||||||
if (idleTimer) clearTimeout(idleTimer);
|
if (idleTimer) clearTimeout(idleTimer);
|
||||||
deps.queue.setActivityTouch?.(chatJid, null);
|
deps.queue.setActivityTouch?.(chatJid, null);
|
||||||
deps.queue.setFollowUpPipeAllowed?.(chatJid, null);
|
deps.queue.setFollowUpPipeAllowed?.(chatJid, null);
|
||||||
clearPendingFollowUp();
|
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) {
|
if (!producedDeliverySucceeded) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
{ chatJid, group: group.name, groupFolder: group.folder, runId },
|
{ chatJid, group: group.name, groupFolder: group.folder, runId },
|
||||||
@@ -1242,8 +1171,6 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
|
|||||||
groupFolder: group.folder,
|
groupFolder: group.folder,
|
||||||
runId,
|
runId,
|
||||||
visiblePhase: settledVisiblePhase,
|
visiblePhase: settledVisiblePhase,
|
||||||
silentRolloverCount,
|
|
||||||
totalSilentWaitMs,
|
|
||||||
},
|
},
|
||||||
'Queued run completed successfully',
|
'Queued run completed successfully',
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user