Skip to content

fix(signal): enable inbound status reactions#72025

Closed
Hua688 wants to merge 1 commit into
openclaw:mainfrom
Hua688:feature/signal-status-reactions
Closed

fix(signal): enable inbound status reactions#72025
Hua688 wants to merge 1 commit into
openclaw:mainfrom
Hua688:feature/signal-status-reactions

Conversation

@Hua688

@Hua688 Hua688 commented Apr 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Describe the problem and fix in 2-5 bullets:

  • Problem: Signal has reaction send helpers, but inbound runs never wired messages.statusReactions.enabled into the shared status reaction lifecycle.
  • Why it matters: Signal users get no queued/thinking/tool/compacting/done/error visual feedback during long-running agent turns even when status reactions are configured.
  • What changed: Create a Signal status reaction controller for eligible inbound messages, route lifecycle callbacks through dispatchInboundMessage, and send reactions via sendReactionSignal for direct and group messages.
  • What did NOT change (scope boundary): No config shape changes, no Signal REST/native transport changes, and no agent/tool message-action behavior changes.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

Root Cause (if applicable)

  • Root cause: Signal's inbound handler dispatched messages without creating a StatusReactionController or passing lifecycle callbacks (onReplyStart, onToolStart, compaction callbacks, terminal state handling) into dispatchInboundMessage.
  • Missing detection / guardrail: Signal had send-reaction helper tests, but no inbound regression test for messages.statusReactions.enabled.
  • Contributing context (if known): Other channels already wire the shared status reaction controller; Signal had the transport primitive but not the inbound lifecycle wiring.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: extensions/signal/src/monitor/event-handler.status-reactions.test.ts
  • Scenario the test should lock in: Signal direct and group inbound messages with messages.statusReactions.enabled send queued and done reactions through the correct direct recipient or groupId/targetAuthor route.
  • Why this is the smallest reliable guardrail: It exercises the Signal inbound handler, dispatch callback wiring, and Signal reaction transport seam without needing a live Signal daemon.
  • Existing test that already covers this (if any): N/A
  • If no new test is added, why not: N/A

User-visible / Behavior Changes

Signal now honors messages.statusReactions.enabled for inbound runs by updating reactions on the triggering message during the agent lifecycle.

Diagram (if applicable)

Before:
Signal inbound -> dispatchInboundMessage -> no status reaction callbacks -> no lifecycle reactions

After:
Signal inbound -> StatusReactionController -> dispatch lifecycle callbacks -> sendReactionSignal -> queued/done status reactions

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No
  • If any Yes, explain risk + mitigation: N/A

Repro + Verification

Environment

  • OS: Linux / Ubuntu 24.04
  • Runtime/container: Node 22 + pnpm checkout
  • Model/provider: N/A for regression test
  • Integration/channel (if any): Signal
  • Relevant config (redacted):
{
  "messages": {
    "ackReaction": "👀",
    "ackReactionScope": "all",
    "statusReactions": {
      "enabled": true
    }
  },
  "channels": {
    "signal": {
      "dmPolicy": "open",
      "allowFrom": ["*"]
    }
  }
}

Steps

  1. Configure Signal with messages.statusReactions.enabled: true.
  2. Send a Signal DM or allowed Signal group message that triggers an agent run.
  3. Observe reactions on the inbound message during processing and completion.

Expected

  • Signal sends lifecycle status reactions such as queued/ack and done (plus intermediate thinking/tool/compacting when those callbacks fire).

Actual

  • Before this fix, Signal sent no automatic lifecycle status reactions because the inbound dispatch path did not wire status reaction callbacks.

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Targeted verification:

  • pnpm test extensions/signal/src/monitor/event-handler.status-reactions.test.ts
  • pnpm lint:extensions

Human Verification (required)

What you personally verified (not just CI), and how:

  • Verified scenarios: Direct Signal inbound status reaction routing and group Signal inbound status reaction routing through the new regression test.
  • Edge cases checked: Direct recipient uses sender recipient; group reaction uses empty recipient plus groupId and targetAuthor.
  • What you did not verify: Live Signal daemon/container end-to-end reaction delivery.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

If a bot review conversation is addressed by this PR, resolve that conversation yourself. Do not leave bot review conversation cleanup for maintainers.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No
  • If yes, exact upgrade steps: N/A

Risks and Mitigations

  • Risk: Signal reaction API failures could add noisy logs during long runs.
    • Mitigation: Reaction send failures are caught and logged through existing verbose logging; they do not fail message dispatch.

@openclaw-barnacle openclaw-barnacle Bot added channel: signal Channel integration: signal size: M labels Apr 26, 2026
@greptile-apps

greptile-apps Bot commented Apr 26, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR wires StatusReactionController into the Signal inbound dispatch path so that Signal users see lifecycle emoji reactions (queued/thinking/tool/done/error) during agent turns when messages.statusReactions.enabled is configured. The implementation correctly mirrors the Telegram/Slack patterns for controller setup, adapter wiring, and early thinking debounce.

  • P1 – spurious done reaction on non-final dispatch: The finally block calls statusReactions.setDone() for every non-error exit, including the !queuedFinal branch that already calls restoreInitial() and returns. Since both are void fire-and-forget, setDone() races with and overrides restoreInitial(), stamping a ✅ done emoji on messages that received no final reply.

Confidence Score: 3/5

Needs a fix before merging: the finally block incorrectly calls setDone() on the non-final-reply early-return path, showing a spurious done reaction.

One P1 logic bug in the core changed file brings the ceiling to 4; the absence of a test for the affected branch drops it further to 3.

extensions/signal/src/monitor/event-handler.ts — the try/catch/finally status reaction termination logic

Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/signal/src/monitor/event-handler.ts
Line: 441-448

Comment:
**`setDone()` fires unconditionally even on the `!queuedFinal` early-return path**

When `dispatchInboundMessage` returns `queuedFinal: false`, the try block calls `statusReactions.restoreInitial()` and then `return`. Because the `finally` block always executes before the return, `statusReactions.setDone()` is also fired (since `dispatchError` is `false`). Both calls are `void` fire-and-forget, so `setDone()` races with — and typically overrides — `restoreInitial()`, leaving a ✅ done emoji on messages that received no final reply. Slack (`dispatch.ts`) and Telegram (`bot-message-dispatch.ts`) avoid this by tracking terminal state and calling `setDone`/`restoreInitial` after the try/catch block, not inside `finally`. The fix is to gate the `finally` branch on whether `queuedFinal` was reached, or restructure to match the Slack/Telegram pattern of deciding the terminal transition only after all paths are known.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: extensions/signal/src/monitor/event-handler.status-reactions.test.ts
Line: 15-17

Comment:
**`!queuedFinal` path for status reactions is not tested**

Both test cases mock `dispatchInboundMessage` to always return `{ queuedFinal: true }`, so the early-return branch (`!queuedFinal``restoreInitial()`) is never exercised. Adding a third case where the mock returns `queuedFinal: false` would have caught the `finally`-block double-fire described above and would lock in the correct "no done reaction on non-final dispatch" contract.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "Signal: add status reactions" | Re-trigger Greptile

Comment on lines 441 to 448
} finally {
if (statusReactions) {
if (dispatchError) {
void statusReactions.setError();
} else {
void statusReactions.setDone();
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 setDone() fires unconditionally even on the !queuedFinal early-return path

When dispatchInboundMessage returns queuedFinal: false, the try block calls statusReactions.restoreInitial() and then return. Because the finally block always executes before the return, statusReactions.setDone() is also fired (since dispatchError is false). Both calls are void fire-and-forget, so setDone() races with — and typically overrides — restoreInitial(), leaving a ✅ done emoji on messages that received no final reply. Slack (dispatch.ts) and Telegram (bot-message-dispatch.ts) avoid this by tracking terminal state and calling setDone/restoreInitial after the try/catch block, not inside finally. The fix is to gate the finally branch on whether queuedFinal was reached, or restructure to match the Slack/Telegram pattern of deciding the terminal transition only after all paths are known.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/signal/src/monitor/event-handler.ts
Line: 441-448

Comment:
**`setDone()` fires unconditionally even on the `!queuedFinal` early-return path**

When `dispatchInboundMessage` returns `queuedFinal: false`, the try block calls `statusReactions.restoreInitial()` and then `return`. Because the `finally` block always executes before the return, `statusReactions.setDone()` is also fired (since `dispatchError` is `false`). Both calls are `void` fire-and-forget, so `setDone()` races with — and typically overrides — `restoreInitial()`, leaving a ✅ done emoji on messages that received no final reply. Slack (`dispatch.ts`) and Telegram (`bot-message-dispatch.ts`) avoid this by tracking terminal state and calling `setDone`/`restoreInitial` after the try/catch block, not inside `finally`. The fix is to gate the `finally` branch on whether `queuedFinal` was reached, or restructure to match the Slack/Telegram pattern of deciding the terminal transition only after all paths are known.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +15 to +17
await params.replyOptions?.onToolStart?.({ name: "read_file" });
return { queuedFinal: true, counts: { tool: 1, block: 0, final: 1 } };
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 !queuedFinal path for status reactions is not tested

Both test cases mock dispatchInboundMessage to always return { queuedFinal: true }, so the early-return branch (!queuedFinalrestoreInitial()) is never exercised. Adding a third case where the mock returns queuedFinal: false would have caught the finally-block double-fire described above and would lock in the correct "no done reaction on non-final dispatch" contract.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/signal/src/monitor/event-handler.status-reactions.test.ts
Line: 15-17

Comment:
**`!queuedFinal` path for status reactions is not tested**

Both test cases mock `dispatchInboundMessage` to always return `{ queuedFinal: true }`, so the early-return branch (`!queuedFinal``restoreInitial()`) is never exercised. Adding a third case where the mock returns `queuedFinal: false` would have caught the `finally`-block double-fire described above and would lock in the correct "no done reaction on non-final dispatch" contract.

How can I resolve this? If you propose a fix, please make it concise.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3cee97dfed

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +445 to +446
} else {
void statusReactions.setDone();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Guard done reaction on actual final delivery

The finally block always calls statusReactions.setDone() when dispatchError is false, even when queuedFinal is false and the code just called restoreInitial() in the early-return path. In silent-success turns (no final response queued), this incorrectly shows a terminal done emoji and overrides the intended restore behavior, so users see completion feedback for turns that produced no final reply.

Useful? React with 👍 / 👎.

isDirect: !entry.isGroup,
isGroup: entry.isGroup,
isMentionableGroup: entry.isGroup,
requireMention: false,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pass mention requirement into status reaction gating

Status reaction enablement calls shouldAckReaction with requireMention: false; with the default messages.ackReactionScope of group-mentions, that gate will always return false for Signal groups, so mentioned group messages never get lifecycle reactions. This hardcoded value bypasses the channel’s real mention policy/decision and makes the new feature unexpectedly inert in common group configurations.

Useful? React with 👍 / 👎.

@clawsweeper

clawsweeper Bot commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Thanks for the context here. I swept through the related work, and this is now duplicate or superseded.

Close this older branch as superseded by the newer maintainer-labeled Signal status-reactions PR, which covers the same user problem with stronger terminal-state handling and real Signal proof.

Root-cause cluster
Relationship: superseded
Canonical: #98791
Summary: This PR and the newer maintainer-labeled PR target the same Signal inbound status-reaction gap; the newer PR is canonical because it is open, proof-positive, and uses safer finalization semantics.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Canonical path: Close this branch and use #98791 as the canonical landing path for Signal inbound status reactions.

So I’m closing this here and keeping the remaining discussion on #98791.

Review details

Best possible solution:

Close this branch and use #98791 as the canonical landing path for Signal inbound status reactions.

Do we have a high-confidence way to reproduce the issue?

Yes, source-level. Current main dispatches Signal inbound messages without status-reaction lifecycle callbacks, while sibling channels and the canonical PR show the expected controller wiring path.

Is this the best way to solve the issue?

No. The Signal plugin is the right owner, but this branch is not the best fix because the newer canonical PR handles visible-delivery finalization and includes live Signal proof.

Security review:

Security review cleared: No concrete security or supply-chain concern found; the diff is limited to Signal runtime wiring and tests with no dependency, workflow, lockfile, secret, or permission changes.

AGENTS.md: found and applied where relevant.

What I checked:

Likely related people:

  • jesse-merhi: Authored the newer maintainer-labeled canonical PR and posted the maintainer decision accepting that branch over this older implementation. (role: current canonical fix owner; confidence: high; commits: d14071f2dfb7; files: extensions/signal/src/monitor/event-handler.ts, extensions/signal/src/monitor/event-handler.inbound-context.test.ts, src/auto-reply/reply/dispatch-from-config.ts)
  • thewilloftheshadow: Merged the shared status-reaction controller work that Signal is trying to reuse and carried the final stall/gating fixes in the merged controller PR. (role: shared status-reaction feature contributor; confidence: high; commits: 30a0d3fce18c, 20f81fdcdedc, a1429a4dd1a5; files: src/channels/status-reactions.ts, src/plugin-sdk/channel-feedback.ts, extensions/telegram/src/bot-message-context.ts)
  • steipete: Recent GitHub commit history shows substantial work in Signal inbound handling, Signal reaction helpers, and the channel dispatch/runtime surfaces involved in this behavior. (role: recent Signal and channel runtime contributor; confidence: high; commits: 1507a9701b83, 827b0de0ce74, ddde144cb65a; files: extensions/signal/src/monitor/event-handler.ts, extensions/signal/src/send-reactions.ts, src/plugin-sdk/channel-feedback.ts)
  • Hua688: Beyond authoring this PR, prior merged history includes Signal REST/container API work on the same reaction transport area used by status reactions. (role: Signal transport contributor and source branch author; confidence: medium; commits: dff4a04c1f5f; files: extensions/signal/src/send-reactions.ts, docs/channels/signal.md)
  • scoootscooob: Moved Signal channel implementation into the current extension owner boundary that this PR changes. (role: Signal extension refactor contributor; confidence: medium; commits: 4540c6b3bc12; files: extensions/signal/src/monitor/event-handler.ts, extensions/signal/src/send-reactions.ts)

Codex review notes: model internal, reasoning high; reviewed against 36dd9ee3c3c1.

@Hua688
Hua688 force-pushed the feature/signal-status-reactions branch from f95f983 to ed7b702 Compare May 1, 2026 06:03
@Hua688
Hua688 requested review from a team as code owners May 1, 2026 06:03
@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation channel: discord Channel integration: discord channel: slack Channel integration: slack channel: telegram Channel integration: telegram app: android App: android app: ios App: ios app: macos App: macos app: web-ui App: web-ui gateway Gateway runtime extensions: memory-core Extension: memory-core cli CLI command changes scripts Repository scripts commands Command implementations agents Agent runtime and tooling extensions: openai extensions: deepseek extensions: qa-lab extensions: codex labels May 1, 2026
@Hua688
Hua688 force-pushed the feature/signal-status-reactions branch from bae67b8 to bdc7cb5 Compare May 19, 2026 03:15
@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation channel: discord Channel integration: discord channel: googlechat Channel integration: googlechat channel: imessage Channel integration: imessage channel: line Channel integration: line channel: matrix Channel integration: matrix channel: mattermost Channel integration: mattermost channel: msteams Channel integration: msteams channel: nextcloud-talk Channel integration: nextcloud-talk channel: nostr Channel integration: nostr labels May 19, 2026
@github-actions github-actions Bot added the dependencies-changed PR changes dependency-related files label May 19, 2026
@openclaw-barnacle openclaw-barnacle Bot added channel: slack Channel integration: slack channel: telegram Channel integration: telegram channel: tlon Channel integration: tlon channel: voice-call Channel integration: voice-call channel: whatsapp-web Channel integration: whatsapp-web channel: zalo Channel integration: zalo channel: zalouser Channel integration: zalouser app: ios App: ios app: macos App: macos extensions: copilot-proxy Extension: copilot-proxy labels May 19, 2026
@clawsweeper

clawsweeper Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper PR egg

🎁 Pass real behavior proof to wake the egg and unlock a hatchable treat.

Where did the egg go?
  • The egg game starts only after the PR passes the real-behavior proof check.
  • Before that, no creature or rarity is rolled. The treat waits for real proof.
  • This is still just collectible flavor: proof affects review readiness, not creature quality.

@clawsweeper

clawsweeper Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper applied the proposed close for this PR.

@jesse-merhi

Copy link
Copy Markdown
Member

Closing this as superseded by #98791, which is the newer canonical Signal status-reactions implementation and has now shipped/merged. Thanks for the earlier work here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: signal Channel integration: signal merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. P2 Normal backlog priority with limited blast radius. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: M status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. triage: needs-pr-context Candidate: external PR body lacks required problem context or evidence. triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Signal status reactions do not run when messages.statusReactions.enabled is true

2 participants