mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 20:35:39 -06:00
b381559184
* fix(auto-reply): don't block reply completion on transcript mirror mirrorTranscriptAfterDispatcherDelivery() opens with `await dispatcher.waitForIdle()` and is awaited from sendFinalPayload, i.e. before the reply operation completes. When a follow-up is queued (queueDepth>=2) the dispatcher cannot go idle until this operation clears, and the operation cannot clear until sendFinalPayload returns — a completion<->dispatcher-idle deadlock. The reply operation stays phase=running and surfaces as stalled_agent_run (recovery=none) until the ~15-minute stuck-session abort. Run the transcript mirror as fire-and-forget: it is post-delivery bookkeeping and does not need to gate completion. The operation completes immediately, the queued follow-up proceeds, the dispatcher goes idle, and the detached mirror then records the transcript. Validated live across three backends (LMStudio openai-completions, claude-cli, DeepSeek cloud): queueDepth 2 and 3, zero stalls. * test(auto-reply): regression coverage for detached transcript mirror Two focused tests on the queueDepth>=2 completion<->dispatcher-idle deadlock: - final reply completes even when dispatcher.waitForIdle() never resolves (pre-fix this awaited-mirror path deadlocked; the test hangs on the old code). - a rejecting background mirror does not fail the reply (the detached .catch contains it instead of surfacing an unhandled rejection). * fix(auto-reply): settle transcript mirrors after delivery Co-authored-by: Zaytsev Ivan <ivan@jad.ru> --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
56 lines
1.8 KiB
TypeScript
56 lines
1.8 KiB
TypeScript
// Reply dispatcher lifecycle helpers used by auto-reply dispatch paths.
|
|
import type { ReplyDispatcher } from "./reply/reply-dispatcher.types.js";
|
|
|
|
type ReplyDispatcherSettledTask = () => Promise<void> | void;
|
|
|
|
const settledTasksByDispatcher = new WeakMap<ReplyDispatcher, Set<ReplyDispatcherSettledTask>>();
|
|
|
|
/** Register post-delivery work owned by the dispatcher's settle lifecycle. */
|
|
export function registerReplyDispatcherSettledTask(
|
|
dispatcher: ReplyDispatcher,
|
|
task: ReplyDispatcherSettledTask,
|
|
): void {
|
|
const tasks = settledTasksByDispatcher.get(dispatcher) ?? new Set<ReplyDispatcherSettledTask>();
|
|
tasks.add(task);
|
|
settledTasksByDispatcher.set(dispatcher, tasks);
|
|
}
|
|
|
|
async function runReplyDispatcherSettledTasks(dispatcher: ReplyDispatcher): Promise<void> {
|
|
const tasks = settledTasksByDispatcher.get(dispatcher);
|
|
if (!tasks) {
|
|
return;
|
|
}
|
|
settledTasksByDispatcher.delete(dispatcher);
|
|
for (const task of tasks) {
|
|
await task();
|
|
}
|
|
}
|
|
|
|
/** Mark a dispatcher complete, wait for pending work, then run optional cleanup. */
|
|
export async function settleReplyDispatcher(params: {
|
|
dispatcher: ReplyDispatcher;
|
|
onSettled?: () => void | Promise<void>;
|
|
}): Promise<void> {
|
|
params.dispatcher.markComplete();
|
|
try {
|
|
await params.dispatcher.waitForIdle();
|
|
await runReplyDispatcherSettledTasks(params.dispatcher);
|
|
} finally {
|
|
settledTasksByDispatcher.delete(params.dispatcher);
|
|
await params.onSettled?.();
|
|
}
|
|
}
|
|
|
|
/** Run work with a dispatcher and always drain it before returning or throwing. */
|
|
export async function withReplyDispatcher<T>(params: {
|
|
dispatcher: ReplyDispatcher;
|
|
run: () => Promise<T>;
|
|
onSettled?: () => void | Promise<void>;
|
|
}): Promise<T> {
|
|
try {
|
|
return await params.run();
|
|
} finally {
|
|
await settleReplyDispatcher(params);
|
|
}
|
|
}
|