simplify runtime turn and progress handling

This commit is contained in:
Eyejoker
2026-03-23 02:19:48 +09:00
parent b83cc95963
commit 1a702d61e3
3 changed files with 313 additions and 35 deletions

View File

@@ -129,7 +129,43 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
const deliverOpenWorkItem = async (
channel: Channel,
item: WorkItem,
options?: {
replaceMessageId?: string | null;
},
): Promise<boolean> => {
const replaceMessageId = options?.replaceMessageId ?? null;
try {
if (replaceMessageId && channel.editMessage) {
await channel.editMessage(
item.chat_jid,
replaceMessageId,
item.result_payload,
);
markWorkItemDelivered(item.id, replaceMessageId);
logger.info(
{
chatJid: item.chat_jid,
workItemId: item.id,
deliveryAttempts: item.delivery_attempts + 1,
replacedMessageId: replaceMessageId,
},
'Delivered produced work item by replacing tracked progress message',
);
return true;
}
} catch (err) {
logger.warn(
{
chatJid: item.chat_jid,
workItemId: item.id,
deliveryAttempts: item.delivery_attempts + 1,
replacedMessageId: replaceMessageId,
err,
},
'Failed to replace tracked progress message; falling back to a new message',
);
}
try {
await channel.sendMessage(item.chat_jid, item.result_payload);
markWorkItemDelivered(item.id);
@@ -637,6 +673,8 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
let progressMessageId: string | null = null;
let progressStartedAt: number | null = null;
let progressTicker: ReturnType<typeof setInterval> | null = null;
let progressEditFailCount = 0;
let promotableTrackedProgressMessageId: string | null = null;
let finalOutputSentToUser = false;
let progressOutputSentToUser = false;
let latestModelProgressTextForFinalFallback: string | null = null;
@@ -644,6 +682,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
let producedDeliverySucceeded = true;
let isFirstLogicalTurn = true;
let poisonedSessionDetected = false;
let canPipeFollowUps = true;
const isClaudeCodeAgent =
(group.agentType || 'claude-code') === 'claude-code';
const FOLLOW_UP_NO_OUTPUT_WARN_MS = 10_000;
@@ -801,6 +840,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
noteFollowUpQueued(meta);
resetIdleTimer();
});
deps.queue.setFollowUpPipeAllowed?.(chatJid, () => canPipeFollowUps);
const clearProgressTicker = () => {
if (progressTicker) {
@@ -817,7 +857,9 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
producedFinalText = null;
};
const flushProducedFinalText = async () => {
const flushProducedFinalText = async (
replaceMessageId?: string | null,
) => {
if (!producedFinalText) {
return;
}
@@ -831,7 +873,9 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
end_seq: isFirstLogicalTurn ? endSeq : null,
result_payload: producedFinalText,
});
const delivered = await deliverOpenWorkItem(channel, workItem);
const delivered = await deliverOpenWorkItem(channel, workItem, {
replaceMessageId,
});
if (delivered) {
finalOutputSentToUser = true;
} else {
@@ -846,6 +890,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
} finally {
producedFinalText = null;
latestModelProgressTextForFinalFallback = null;
promotableTrackedProgressMessageId = null;
isFirstLogicalTurn = false;
}
};
@@ -856,6 +901,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
latestProgressRendered = null;
progressMessageId = null;
progressStartedAt = null;
progressEditFailCount = 0;
};
const renderProgressMessage = (text: string) => {
@@ -888,7 +934,9 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
try {
await channel.editMessage(chatJid, progressMessageId, rendered);
latestProgressRendered = rendered;
progressEditFailCount = 0;
} catch (err) {
progressEditFailCount++;
logger.warn(
{
chatJid,
@@ -896,13 +944,16 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
groupFolder: group.folder,
runId,
progressMessageId,
progressEditFailCount,
err,
},
'Failed to edit tracked progress message; will recreate it on the next progress update',
'Failed to edit tracked progress message; will retry before recreating',
);
clearProgressTicker();
progressMessageId = null;
latestProgressRendered = null;
if (progressEditFailCount >= 3) {
clearProgressTicker();
progressMessageId = null;
}
}
};
@@ -916,7 +967,9 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
}, 10_000);
};
const finalizeProgressMessage = async () => {
const finalizeProgressMessage = async (options?: {
preserveTrackedMessage?: boolean;
}): Promise<string | null> => {
logger.info(
{
chatJid,
@@ -928,8 +981,12 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
},
'Finalizing tracked progress message',
);
await syncTrackedProgressMessage();
const trackedMessageId = progressMessageId;
if (!options?.preserveTrackedMessage) {
await syncTrackedProgressMessage();
}
resetProgressState();
return trackedMessageId;
};
const sendProgressMessage = async (text: string) => {
@@ -963,6 +1020,9 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
}
if (!channel.sendAndTrack) {
latestProgressRendered = rendered;
await channel.sendMessage(chatJid, rendered);
progressOutputSentToUser = true;
return;
}
@@ -979,6 +1039,9 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
},
'Failed to send tracked progress message',
);
latestProgressRendered = rendered;
await channel.sendMessage(chatJid, rendered);
progressOutputSentToUser = true;
return;
}
@@ -997,7 +1060,12 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
latestProgressRendered = rendered;
ensureProgressTicker();
progressOutputSentToUser = true;
return;
}
latestProgressRendered = rendered;
await channel.sendMessage(chatJid, rendered);
progressOutputSentToUser = true;
};
await channel.setTyping?.(chatJid, true);
@@ -1032,6 +1100,9 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
? result.result
: JSON.stringify(result.result);
const text = formatOutbound(raw);
if (text) {
canPipeFollowUps = false;
}
logger.info(
{
chatJid,
@@ -1073,9 +1144,13 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
sawNonProgressOutput = true;
if (text) {
await finalizeProgressMessage();
const trackedProgressMessageId = await finalizeProgressMessage({
preserveTrackedMessage: !!(
progressMessageId && channel.editMessage
),
});
producedFinalText = text;
await flushProducedFinalText();
await flushProducedFinalText(trackedProgressMessageId);
} else {
logger.info(
{
@@ -1096,7 +1171,14 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
if (result.status === 'success') {
warnFollowUpEndedWithoutOutput('success-null-result');
}
await finalizeProgressMessage();
promotableTrackedProgressMessageId = await finalizeProgressMessage({
preserveTrackedMessage: !!(
!sawNonProgressOutput &&
latestModelProgressTextForFinalFallback &&
progressMessageId &&
channel.editMessage
),
});
}
await channel.setTyping?.(chatJid, false);
@@ -1106,6 +1188,12 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
if (result.status === 'success' && !poisonedSessionDetected) {
deps.queue.notifyIdle(chatJid, runId);
if (finalOutputSentToUser || progressOutputSentToUser) {
deps.queue.closeStdin(chatJid, {
runId,
reason: 'output-delivered-close',
});
}
}
if (result.status === 'error') {
@@ -1136,14 +1224,20 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
},
'Promoting last progress output to final message after agent completion',
);
const trackedProgressMessageId = await finalizeProgressMessage({
preserveTrackedMessage: !!(progressMessageId && channel.editMessage),
});
producedFinalText = latestModelProgressTextForFinalFallback;
await flushProducedFinalText();
await flushProducedFinalText(
trackedProgressMessageId || promotableTrackedProgressMessageId,
);
}
clearProgressTicker();
if (idleTimer) clearTimeout(idleTimer);
deps.queue.setActivityTouch?.(chatJid, null);
deps.queue.setFollowUpPipeAllowed?.(chatJid, null);
const finalFollowUpRequeueReason =
followUpEndedWithoutOutputReason ||