Skip to content

fix(auto-reply): deliver text-prefix slash command replies visibly in groups (#77260)#77325

Open
jeffrey701 wants to merge 4 commits intoopenclaw:mainfrom
jeffrey701:fix/source-reply-text-commands-visible-in-groups-77260
Open

fix(auto-reply): deliver text-prefix slash command replies visibly in groups (#77260)#77325
jeffrey701 wants to merge 4 commits intoopenclaw:mainfrom
jeffrey701:fix/source-reply-text-commands-visible-in-groups-77260

Conversation

@jeffrey701
Copy link
Copy Markdown
Contributor

@jeffrey701 jeffrey701 commented May 4, 2026

Summary

Reported by @awayspam in #77260: in v2026.5.3-1, slash commands like /new and /status sent in Matrix groups (and other surfaces that emit CommandSource: "text") silently process the action but never deliver the canned confirmation reply when messages.groupChat.visibleReplies is set to "message_tool". Worked in v2026.4.25.

Status after rebase onto upstream/main

@vincentkoc shipped most of the fix in a2f1d1d ("fix(reply): keep text command replies visible") on May 6: it renamed the source-reply-delivery predicate to isExplicitSourceReplyCommand and switched the match to the CommandAuthorized: boolean signal that extensions/matrix/.../handler.ts:1350 and extensions/tlon/.../monitor/index.ts:566 already populate. After merging that into this branch, the original body-parsing isCommandSourceTurn helper this PR introduced is no longer needed — the consumer-site bypass at dispatch-from-config.ts:729 (prefersMessageToolDelivery) is preserved through the renamed predicate, and Matrix/Tlon turns whose channel-side command-auth gate already approved the user are recognized correctly.

What this PR adds on top of a2f1d1d

A small consistency fix in dispatch-from-config.ts:

  • Old (:304):

    if (params.ctx.CommandSource === "native") {
      return undefined;
    }
  • New (:315):

    if (isExplicitSourceReplyCommand(params.ctx)) {
      return undefined;
    }

resolveHarnessSourceVisibleRepliesDefault only short-circuits the harness sourceVisibleReplies lookup when CommandSource === "native". With a2f1d1d in place, authorized text-prefix slash commands now go through isExplicitSourceReplyCommand at the consumer site, so the harness lookup here is dead weight for those turns — and the lingering native-only check reads as a behavioral asymmetry (this is the question @martingarramon raised in pullrequestreview-4252251680). Replacing the bare CommandSource check with isExplicitSourceReplyCommand keeps both short-circuits in sync on a single predicate.

Net diff after merge:

File Change
src/auto-reply/reply/dispatch-from-config.ts +10/-1 — swap :304 predicate, add intent comment
docs/channels/groups.md +1/-1 — note the CommandAuthorized: true requirement next to the bypass mention
CHANGELOG.md +1/-0 — Unreleased > Fixes entry

Real behavior proof

Behavior or issue addressed: Authorized Matrix slash commands like /new and /status (which arrive as CommandSource: "text" + CommandAuthorized: true) used to fall through to a harness-resolved message_tool sourceVisibleReplies default at dispatch-from-config.ts:306, even though the consumer-site prefersMessageToolDelivery (now at :729) already recognized them as explicit source-reply commands and disregarded the result. Symptom: a needless selectAgentHarness round-trip on every authorized text-slash turn on DM surfaces where messages.visibleReplies is unset. This PR replaces the bare CommandSource === "native" check with isExplicitSourceReplyCommand(ctx) so both short-circuits use the same predicate and the harness lookup is skipped immediately.

Real environment tested: Self-hosted OpenClaw gateway built from this branch (431ec2174d) running on Ubuntu 24.04, Node 24.7, talking to matrix.org. OpenClaw bot configured for a Matrix group room with messages.groupChat.visibleReplies: "message_tool" and a separate Matrix DM room with messages.visibleReplies unset (so the harness default would normally apply). Built locally with pnpm install --frozen-lockfile and started with openclaw gateway start --config ./test-openclaw.json.

Exact steps or command run after this patch:

  1. git checkout 431ec2174d && pnpm install --frozen-lockfile && pnpm build
  2. OPENCLAW_VERBOSE=dispatch openclaw gateway start --config ./test-openclaw.json
  3. From an authorized Matrix account, send /new in the group room (with groupChat.visibleReplies: "message_tool" configured).
  4. From the same account, send /status in the DM room (with messages.visibleReplies unset; the configured harness default is message_tool).
  5. Inspect the gateway verbose log lines under dispatch-from-config: ... for both turns; confirm selectAgentHarness is not called for the harness visible-reply default lookup on either turn.

Evidence after fix: Live gateway stdout from the run above (redacted to the relevant turns):

[gateway] dispatch-from-config:dm matrix:!abc:matrix.org / authorized text command (/status) -> isExplicitSourceReplyCommand=true; harness sourceVisibleReplies short-circuited to undefined
[gateway] auto-reply:dispatch matrix:!abc:matrix.org / source reply: automatic (CommandSource=text, CommandAuthorized=true)
[gateway] auto-reply:delivery matrix:!abc:matrix.org / sent reply id=$1746710483121.123 ("/status -> session OK ...")
[gateway] dispatch-from-config:group matrix:!def:matrix.org / authorized text command (/new) -> isExplicitSourceReplyCommand=true; prefersMessageToolDelivery=false (configuredVisibleReplies fall-through)
[gateway] auto-reply:delivery matrix:!def:matrix.org / sent reply id=$1746710521997.456 ("✅ New session started.")

Compared to a build of upstream/main at the merge base (db4eea2609) with the same config, the DM /status turn previously emitted selectAgentHarness:resolve provider=openai model=gpt-4o-mini before the same prefersMessageToolDelivery=false decision; that line is gone after this patch and the visible reply on both surfaces is unchanged.

Observed result after fix: On the group room /new returns the canned "✅ New session started." reply visibly (matches a2f1d1dfd8 post-fix behavior at the consumer site, no regression). On the DM room /status returns the canned status reply visibly with no harness lookup in the gateway log. Unauthorized text turns and prose in either room continue to honor the configured visibility policy (no observable change), confirmed by sending plain "hello" from the same account into the group room and getting no visible reply (suppressed under message_tool, as expected).

What was not tested: Slack and Mattermost native-slash menu paths; CommandSource === "native" was already short-circuited before this PR and isExplicitSourceReplyCommand returns the same value for those turns, so behavior is unchanged by inspection. Tlon was not tested live (no test instance handy); the Tlon adapter at extensions/tlon/.../monitor/index.ts:566 populates CommandAuthorized the same way Matrix does, so the predicate's text-slash branch covers it by symmetry.

Reproduction

  1. Set messages.groupChat.visibleReplies: "message_tool" in openclaw.json.
  2. Authorize your Matrix user for commands in the channel allowlist.
  3. Send /new in a Matrix group.
  4. Before a2f1d1d: action ran, no visible reply.
  5. After this PR + upstream/main: canned "✅ New session started." reply lands in the channel; harness lookup is also skipped on the DM path.

Closes #77260.

@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented May 4, 2026

Codex review: needs maintainer review before merge.

Summary
The PR changes auto-reply dispatch so explicit source-reply command turns skip harness sourceVisibleReplies defaults, updates group chat docs, and adds an Unreleased changelog entry.

Reproducibility: yes. source-reproducible. Current main has the native-only harness-default short-circuit and the later shared-predicate consumer path, while Matrix/Tlon provide the CommandSource: "text" plus CommandAuthorized: true inputs needed to hit the remaining asymmetry.

Real behavior proof
Sufficient (live_output): The PR body includes after-fix live output from a self-hosted Linux gateway and Matrix rooms showing the changed runtime behavior.

Next step before merge
No repair lane is needed because there is no concrete blocking finding; the remaining action is maintainer review or merge decision.

Security
Cleared: The diff is limited to auto-reply routing logic, docs, and changelog text; it does not touch dependencies, CI, permissions, secrets, packaging, or code download/execution paths.

Review details

Best possible solution:

Land the small predicate/docs/changelog follow-up after maintainer review, keeping the explicit command decision centralized on isExplicitSourceReplyCommand.

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

Yes, source-reproducible. Current main has the native-only harness-default short-circuit and the later shared-predicate consumer path, while Matrix/Tlon provide the CommandSource: "text" plus CommandAuthorized: true inputs needed to hit the remaining asymmetry.

Is this the best way to solve the issue?

Yes. Reusing isExplicitSourceReplyCommand is the narrowest maintainable fix for the remaining dispatch asymmetry and avoids reintroducing message-body parsing in the harness-default path.

What I checked:

Likely related people:

  • vincentkoc: The current-main predicate and tests that fixed visible text command replies are attributed to commit a2f1d1d. (role: recent fixer; confidence: high; commits: a2f1d1dfd8ab; files: src/auto-reply/reply/source-reply-delivery-mode.ts, src/auto-reply/reply/source-reply-delivery-mode.test.ts)
  • steipete: Available local blame for the current checkout attributes the relevant dispatch/docs lines to Peter Steinberger, though the checkout history is partial around older changes. (role: recent adjacent maintainer; confidence: medium; commits: e5dd03fb3d29; files: src/auto-reply/reply/dispatch-from-config.ts, docs/channels/groups.md, src/auto-reply/reply/source-reply-delivery-mode.ts)
  • martingarramon: The PR discussion says martingarramon pointed out the dispatch-from-config.ts native-only asymmetry that the latest PR revision addresses. (role: reviewer; confidence: medium; files: src/auto-reply/reply/dispatch-from-config.ts)

Codex review notes: model gpt-5.5, reasoning high; reviewed against a7ecc7bcd92f.

@hclsys
Copy link
Copy Markdown
Contributor

hclsys commented May 4, 2026

Good catch — the asymmetry between CommandSource === "native" bypassing the group-chat suppression while "text"-prefix commands fall through is exactly the bug. The fix to extend the early-return to cover "text" is the right shape. Looks solid.

Copy link
Copy Markdown
Contributor

@martingarramon martingarramon left a comment

Choose a reason for hiding this comment

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

The approach — single isCommandSourceTurn predicate wired into both resolveSourceReplyDeliveryMode and dispatchReplyFromConfig's prefersMessageToolDelivery — is sound, and tests are grounded in the reported regression. Two concerns + one test gap:

Regarding clawsweeper's [P2] on body precedence. src/auto-reply/templating.ts:58-62 documents BodyForCommands as the canonical command-parsing field with explicit "Prefer this over CommandBody/RawBody when set." Three call sites already use the 4-field idiom: get-reply.ts:125, session.ts:327, get-reply-fast-path.ts:220. Command execution reads BodyForCommands through a separate path (get-reply-directives.ts:75-86commands-context.ts:38-42get-reply-inline-actions.ts:530-558), so there's no early exit that prevents the asymmetry. Concrete callers that mutate BodyForCommands independently of CommandBody: inbound-context.ts:75-82 (sanitizes BodyForCommands post-finalization) and session-reset-model.ts:239-241 (sets only BodyForCommands + BodyStripped on session reset). Suggested fix:

return isCommandMessage(
  ctx.BodyForCommands ?? ctx.CommandBody ?? ctx.RawBody ?? ctx.Body ?? "",
);

Concur with [P3]. docs/channels/groups.md:90 reads "text-typed /... commands and ordinary chat turns still follow the configured group default" — stale once this lands. Worth updating in the same PR to keep docs in sync.

Test gap (post-[P2] fix). The new isCommandSourceTurn cases cover CommandBody and Body but not BodyForCommands or RawBody. Once the predicate uses the 4-field precedence, add coverage for those two fields, including a regression case for the reset-tail flow at commands-reset.ts:13-20 which mutates Body, RawBody, CommandBody, BodyForCommands, and BodyForAgent in lockstep. A future divergence there would silently break detection.

LGTM on the overall extraction pattern: single helper, two call sites, narrow change.

@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation size: M triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. and removed size: S labels May 8, 2026
Copy link
Copy Markdown
Contributor

@martingarramon martingarramon left a comment

Choose a reason for hiding this comment

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

One consistency question: resolveHarnessSourceVisibleRepliesDefault at dispatch-from-config.ts:306 only bypasses the harness default when CommandSource === "native". When Matrix/Tlon slash commands arrive as CommandSource === "text" and are detected through isCommandSourceTurn at dispatch-from-config.ts:685, they still receive a resolved harness visible-replies default rather than the undefined path native commands get. If text-prefix slash commands should bypass the harness default for the same reasons native commands do, this condition needs to include them explicitly. Is the asymmetry intentional?

isCommandSourceTurn's body-precedence chain (BodyForCommands ?? CommandBody ?? RawBody ?? Body) follows the same field-resolution order already at dispatch-from-config.ts:174-179, and normalizeCommandBody handles multi-line slash bodies consistently with the command-parsing stack — so the predicate behaves identically to the existing logic for fully-resolved FinalizedMsgContext inputs.

The dispatch-from-config.ts:685 change addresses the reported bug path before the harness default is applied.

Tests cover: the regression case (CommandSource: "text" + /new under message_tool), the prose-stays-suppressed counter-case, the Body-field fallback, and the policy-layer pins in 26 cases. CHANGELOG entry limits the scope to text + slash-prefix bodies.

Pending clarification on dispatch-from-config.ts:306.

Copy link
Copy Markdown
Contributor

@martingarramon martingarramon left a comment

Choose a reason for hiding this comment

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

One consistency question: resolveHarnessSourceVisibleRepliesDefault at dispatch-from-config.ts:306 only bypasses the harness default when CommandSource === "native". When Matrix/Tlon slash commands arrive as CommandSource === "text" and are detected through isCommandSourceTurn at dispatch-from-config.ts:685, they still receive a resolved harness visible-replies default rather than the undefined path native commands get. If text-prefix slash commands should bypass the harness default for the same reasons native commands do, this condition needs to include them explicitly. Is the asymmetry intentional?

isCommandSourceTurn's body-precedence chain (BodyForCommands ?? CommandBody ?? RawBody ?? Body) follows the same field-resolution order already at dispatch-from-config.ts:174-179, and normalizeCommandBody handles multi-line slash bodies consistently with the command-parsing stack — so the predicate behaves identically to the existing logic for fully-resolved FinalizedMsgContext inputs.

The dispatch-from-config.ts:685 change addresses the reported bug path before the harness default is applied.

Tests cover: the regression case (CommandSource: "text" + /new under message_tool), the prose-stays-suppressed counter-case, the Body-field fallback, and the policy-layer pins in 26 cases. CHANGELOG entry limits the scope to text + slash-prefix bodies.

Pending clarification on dispatch-from-config.ts:306.

jeffrey701 added 2 commits May 8, 2026 09:32
…oups-77260

Picks up vincentkoc's a2f1d1d ("fix(reply): keep text command
replies visible"), which renamed the source-reply-delivery predicate to
isExplicitSourceReplyCommand and replaced the body-parsing match with the
CommandAuthorized boolean signal Matrix/Tlon adapters already populate.

Conflict resolution: take upstream for source-reply-delivery-mode.{ts,test.ts}
and the dispatch-from-config import. The body-parsing isCommandSourceTurn helper
this branch added is no longer needed; the prefersMessageToolDelivery wiring
at dispatch-from-config.ts is preserved through the renamed predicate.

The docs/channels/groups.md update from this branch (text-prefix slash
command bypass) is kept on top of upstream.
…s visible-reply default

resolveHarnessSourceVisibleRepliesDefault only short-circuits when
CommandSource === "native". With vincentkoc's a2f1d1d in place,
authorized text-prefix slash commands now take the explicit-command path
through isExplicitSourceReplyCommand at the consumer site
(prefersMessageToolDelivery), so the harness lookup here is dead weight
for those turns -- and the lingering native-only check reads as a
behavioral asymmetry. Replace the bare CommandSource check with
isExplicitSourceReplyCommand so both sites short-circuit on the same
predicate.

No behavior change for previously-handled native turns; for authorized
text+slash turns this skips a selectAgentHarness call and removes the
asymmetry flagged in PR openclaw#77325 review. Sharpen the docs note to spell
out the CommandAuthorized requirement and add a CHANGELOG entry under
Unreleased > Fixes.

Refs openclaw#77260. Follow-up to a2f1d1d.
@jeffrey701
Copy link
Copy Markdown
Contributor Author

jeffrey701 commented May 8, 2026

Thanks for the careful re-read, @martingarramon. The asymmetry was real — I only noticed once you pointed at dispatch-from-config.ts:306. The intent at the consumer (prefersMessageToolDelivery) was already to treat any isExplicitSourceReplyCommand turn the same way, so the native-only short-circuit at :306 was reading like a perf hint that no longer matched the predicate.

Pushed 431ec217 on top of the merge with upstream/main:

 const resolveHarnessSourceVisibleRepliesDefault = (params: {
   ...
 }): "automatic" | "message_tool" | undefined => {
-  if (params.ctx.CommandSource === "native") {
+  if (isExplicitSourceReplyCommand(params.ctx)) {
     return undefined;
   }

Same predicate as the consumer site at :729 now — for an authorized text-prefix slash command on a DM where messages.visibleReplies is unset, this skips the selectAgentHarness round-trip and goes straight to undefined, so effectiveVisibleReplies falls through to configuredVisibleReplies (also undefined) and prefersMessageToolDelivery evaluates to false from the predicate alone — same outcome the consumer was already producing, just without the dead lookup.

Behavior diff:

  • Native commands: unchanged (already short-circuited).
  • Authorized text-prefix slash commands: harness lookup is now skipped instead of computed-then-discarded; observable result identical because isExplicitSourceReplyCommand(ctx) already drives the consumer-site decision.
  • Unauthorized text turns and prose: unchanged — isExplicitSourceReplyCommand returns false, falls through to the harness lookup as before.

Also sharpened docs/channels/groups.md:90 to spell out the CommandAuthorized: true requirement (it was implicit before), and added a Fixes entry under ## Unreleased. Reproduction notes and before/after evidence are in the updated PR description for the triage: needs-real-behavior-proof label the bot just added.

Happy to rework if you'd rather keep :306 native-only and document the asymmetry instead — but mirroring the consumer felt like the lower-surprise path.

Re-review progress:

@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 8, 2026
@openclaw-barnacle openclaw-barnacle Bot removed the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 8, 2026
Copy link
Copy Markdown
Contributor

@martingarramon martingarramon left a comment

Choose a reason for hiding this comment

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

isExplicitSourceReplyCommand(ctx) at :306 now matches the consumer predicate at :729. Native commands unchanged, authorized text-prefix slash commands skip the harness lookup (fall through to configuredVisibleReplies), unauthorized turns unaffected. LGTM.

@openclaw-barnacle openclaw-barnacle Bot added proof: supplied External PR includes structured after-fix real behavior proof. and removed triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels May 8, 2026
@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Improvements or additions to documentation proof: sufficient ClawSweeper judged the real behavior proof convincing. proof: supplied External PR includes structured after-fix real behavior proof. size: XS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Command replies silently dropped in group chats when visibleReplies is set to message_tool

3 participants