fix(agents): resolve compaction wait before channel flush (#59308)

Merged via squash.

Prepared head SHA: bf17502df8
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
Gustavo Madeira Santana 2026-04-01 21:40:23 -04:00 committed by GitHub
parent 326490ab76
commit 32fa5c3be5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 50 additions and 11 deletions

View File

@ -27,6 +27,7 @@ Docs: https://docs.openclaw.ai
- Exec approvals: route Slack, Discord, and Telegram approvals through the shared channel approval-capability path so native approval auth, delivery, and `/approve` handling stay aligned across channels while preserving Telegram session-key agent filtering. (#58634) thanks @gumadeiras
- Matrix/runtime: resolve the verification/bootstrap runtime from a distinct packaged Matrix entry so global npm installs stop failing on crypto bootstrap with missing-module or recursive runtime alias errors. (#59249) Thanks @gumadeiras.
- Matrix/streaming: preserve ordered block flushes before tool, message, and agent boundaries, add explicit `channels.matrix.blockStreaming` opt-in so Matrix `streaming: "off"` stays final-only by default, and move MiniMax plain-text final handling into the MiniMax provider runtime instead of the shared core heuristic. (#59266) thanks @gumadeiras
- Agents/compaction: resolve compaction wait before final reply/channel flush completion so slow end-of-run delivery drains no longer delay compaction completion. (#59308) thanks @gumadeiras
## 2026.4.2

View File

@ -9,7 +9,10 @@ vi.mock("../infra/agent-events.js", () => ({
function createContext(
lastAssistant: unknown,
overrides?: { onAgentEvent?: (event: unknown) => void },
overrides?: {
onAgentEvent?: (event: unknown) => void;
onBlockReplyFlush?: () => void | Promise<void>;
},
): EmbeddedPiSubscribeContext {
const onBlockReply = vi.fn();
return {
@ -19,6 +22,7 @@ function createContext(
sessionKey: "agent:main:main",
onAgentEvent: overrides?.onAgentEvent,
onBlockReply,
onBlockReplyFlush: overrides?.onBlockReplyFlush,
},
state: {
lastAssistant: lastAssistant as EmbeddedPiSubscribeContext["state"]["lastAssistant"],
@ -179,4 +183,45 @@ describe("handleAgentEnd", () => {
expect(ctx.state.pendingToolMediaUrls).toEqual([]);
expect(ctx.state.pendingToolAudioAsVoice).toBe(false);
});
it("resolves compaction wait before awaiting an async block reply flush", async () => {
let resolveFlush: (() => void) | undefined;
const ctx = createContext(undefined);
ctx.flushBlockReplyBuffer = vi
.fn()
.mockImplementationOnce(
() =>
new Promise<void>((resolve) => {
resolveFlush = resolve;
}),
)
.mockImplementation(() => {});
const endPromise = handleAgentEnd(ctx);
expect(ctx.maybeResolveCompactionWait).toHaveBeenCalledTimes(1);
expect(ctx.resolveCompactionRetry).not.toHaveBeenCalled();
resolveFlush?.();
await endPromise;
});
it("resolves compaction wait before awaiting an async channel flush", async () => {
let resolveChannelFlush: (() => void) | undefined;
const onBlockReplyFlush = vi.fn(
() =>
new Promise<void>((resolve) => {
resolveChannelFlush = resolve;
}),
);
const ctx = createContext(undefined, { onBlockReplyFlush });
const endPromise = handleAgentEnd(ctx);
expect(ctx.maybeResolveCompactionWait).toHaveBeenCalledTimes(1);
expect(onBlockReplyFlush).toHaveBeenCalledTimes(1);
resolveChannelFlush?.();
await endPromise;
});
});

View File

@ -136,20 +136,13 @@ export function handleAgentEnd(ctx: EmbeddedPiSubscribeContext) {
};
const flushBlockReplyBufferResult = ctx.flushBlockReplyBuffer();
finalizeAgentEnd();
if (isPromiseLike<void>(flushBlockReplyBufferResult)) {
return flushBlockReplyBufferResult
.then(() => flushPendingMediaAndChannel())
.finally(() => {
finalizeAgentEnd();
});
return flushBlockReplyBufferResult.then(() => flushPendingMediaAndChannel());
}
const flushPendingMediaAndChannelResult = flushPendingMediaAndChannel();
if (isPromiseLike<void>(flushPendingMediaAndChannelResult)) {
return flushPendingMediaAndChannelResult.finally(() => {
finalizeAgentEnd();
});
return flushPendingMediaAndChannelResult;
}
finalizeAgentEnd();
}