diff --git a/CHANGELOG.md b/CHANGELOG.md index 67f02518970..a95fa39f5ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/extensions/matrix/src/approval-handler.runtime.test.ts b/extensions/matrix/src/approval-handler.runtime.test.ts index 790866f49e2..8c746f4be0b 100644 --- a/extensions/matrix/src/approval-handler.runtime.test.ts +++ b/extensions/matrix/src/approval-handler.runtime.test.ts @@ -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() diff --git a/extensions/matrix/src/approval-handler.runtime.ts b/extensions/matrix/src/approval-handler.runtime.ts index f03dd91ab84..bdd314ad1be 100644 --- a/extensions/matrix/src/approval-handler.runtime.ts +++ b/extensions/matrix/src/approval-handler.runtime.ts @@ -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 }) => {