fix(matrix): bind approval reactions before option emoji

This commit is contained in:
Vincent Koc
2026-05-03 18:49:36 -07:00
parent 90c0edcb61
commit 1c2eda206e
3 changed files with 55 additions and 1 deletions

View File

@@ -67,6 +67,7 @@ Docs: https://docs.openclaw.ai
- Slack/Matrix: avoid creating blank progress-draft messages when `streaming.progress.label=false` and progress tool lines are disabled. Thanks @vincentkoc.
- Slack/Discord: suppress standalone tool-progress chatter when partial preview streaming has `streaming.preview.toolProgress: false`, matching the documented quiet-preview behavior. Thanks @vincentkoc.
- QA/Matrix: keep the mock OpenAI tool-progress provider aligned with exact-marker Matrix prompts so the hardened live preview scenario still forces a deterministic read before final delivery. Thanks @vincentkoc.
- Matrix: bind native approval reaction targets before publishing option reactions, so fast approver reactions on threaded prompts are not dropped while the approval handler finishes setup. Thanks @vincentkoc.
- Google Meet: make realtime talk-back agent-driven by default with `realtime.strategy: "agent"`, keep the previous direct bidirectional model behavior available as `realtime.strategy: "bidi"`, route the Meet tab speaker output to `BlackHole 2ch` automatically for local Chrome realtime joins, coalesce nearby speech transcript fragments before consulting the agent, and avoid cutting off agent speech from server VAD or stale playback pipe errors.
- Google Meet: suppress queued assistant playback and assistant-like transcript echoes from the realtime input path, so the meeting does not hear the agent's own speech as a new user turn and loop or cut itself off.
- OpenAI/Google Meet: wait for realtime voice `session.updated` before treating the bridge as connected, so Meet joins do not return with audio queued behind an unconfigured realtime session. Thanks @vincentkoc.

View File

@@ -266,6 +266,52 @@ describe("matrixApprovalNativeRuntime", () => {
);
});
it("binds Matrix approval reactions before publishing option reactions", async () => {
const sendSingleTextMessage = vi.fn().mockResolvedValue({
messageId: "$approval",
primaryMessageId: "$approval",
messageIds: ["$approval"],
roomId: "!room:example.org",
});
const reactMessage = vi.fn().mockImplementation(async () => {
expect(
resolveMatrixApprovalReactionTarget({
roomId: "!room:example.org",
eventId: "$approval",
reactionKey: "✅",
}),
).toEqual({
approvalId: "req-1",
decision: "allow-once",
});
});
const view = buildExecApprovalView();
const pendingPayload = await buildPendingPayload(view);
await matrixApprovalNativeRuntime.transport.deliverPending({
cfg: {} as never,
accountId: "default",
context: {
client: {} as never,
deps: {
sendSingleTextMessage,
reactMessage,
},
},
request: {} as never,
approvalKind: "exec",
plannedTarget: buildMatrixApprovalRoomTarget("!room:example.org"),
preparedTarget: {
to: "room:!room:example.org",
roomId: "!room:example.org",
},
view,
pendingPayload,
});
expect(reactMessage).toHaveBeenCalled();
});
it("falls back to chunked Matrix delivery when approval content exceeds one event", async () => {
const sendSingleTextMessage = vi
.fn()

View File

@@ -408,7 +408,7 @@ export const matrixApprovalNativeRuntime = createChannelApprovalNativeRuntimeAda
: null,
);
},
deliverPending: async ({ cfg, accountId, context, preparedTarget, pendingPayload }) => {
deliverPending: async ({ cfg, accountId, context, preparedTarget, pendingPayload, view }) => {
const resolved = resolveHandlerContext({ cfg, accountId, context });
if (!resolved) {
return null;
@@ -447,6 +447,13 @@ export const matrixApprovalNativeRuntime = createChannelApprovalNativeRuntimeAda
);
const reactionEventId =
result.primaryMessageId?.trim() || messageIds[0] || result.messageId.trim();
registerMatrixApprovalReactionTarget({
roomId: result.roomId,
eventId: reactionEventId,
approvalId: pendingPayload.approvalId,
allowedDecisions: pendingPayload.allowedDecisions,
ttlMs: view.expiresAtMs - Date.now(),
});
await Promise.allSettled(
listMatrixApprovalReactionBindings(pendingPayload.allowedDecisions).map(
async ({ emoji }) => {