feat: recover paired executions after restart

This commit is contained in:
Eyejoker
2026-03-29 05:06:25 +09:00
parent bb30330c66
commit 29310ce67f
4 changed files with 537 additions and 0 deletions

View File

@@ -45,6 +45,7 @@ import {
approveRoomPlan,
formatRoomReviewReadyMessage,
markRoomReviewReady,
planPairedExecutionRecovery,
recordRoomPlan,
requestRoomPlanChanges,
setRoomTaskRiskLevel,
@@ -965,6 +966,7 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
chatJid,
resolveGroupIpcPath(group.folder),
);
continue;
} else if (rawPending.length > 0) {
const endSeq = rawPending[rawPending.length - 1].seq;
if (endSeq != null) {
@@ -976,6 +978,58 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
);
}
}
const lease = getEffectiveChannelLease(chatJid);
const roomRoleContext = buildRoomRoleContext(lease, SERVICE_ID);
const recoveryPlan = planPairedExecutionRecovery({
group,
chatJid,
roomRoleContext,
});
if (!recoveryPlan) {
continue;
}
deps.queue.enqueueTask(chatJid, recoveryPlan.recoveryKey, async () => {
const recoveryChannel = findChannel(deps.channels, chatJid);
if (!recoveryChannel) {
logger.warn(
{
chatJid,
taskId: recoveryPlan.task.id,
role: recoveryPlan.role,
recoveryKey: recoveryPlan.recoveryKey,
},
'Skipping paired execution recovery because no channel owns the chat',
);
return;
}
const runId = `recovery-${Date.now().toString(36)}-${Math.random()
.toString(36)
.slice(2, 8)}`;
logger.info(
{
chatJid,
group: group.name,
taskId: recoveryPlan.task.id,
role: recoveryPlan.role,
checkpointFingerprint: recoveryPlan.checkpointFingerprint,
recoveryKey: recoveryPlan.recoveryKey,
runId,
},
'Recovery: resuming paired execution after restart',
);
await executeTurn({
group,
prompt: recoveryPlan.prompt,
chatJid,
runId,
channel: recoveryChannel,
startSeq: null,
endSeq: null,
});
});
}
};