fix(auto-reply): deliver text-prefix slash command replies visibly in groups (#77260)#77325
Conversation
|
Codex review: needs maintainer review before merge. Summary 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 Real behavior proof Next step before merge Security Review detailsBest possible solution: Land the small predicate/docs/changelog follow-up after maintainer review, keeping the explicit command decision centralized on 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 Is this the best way to solve the issue? Yes. Reusing What I checked:
Likely related people:
Codex review notes: model gpt-5.5, reasoning high; reviewed against a7ecc7bcd92f. |
|
Good catch — the asymmetry between |
martingarramon
left a comment
There was a problem hiding this comment.
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-86 → commands-context.ts:38-42 → get-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.
…review feedback from openclaw#77325)
martingarramon
left a comment
There was a problem hiding this comment.
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.
martingarramon
left a comment
There was a problem hiding this comment.
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.
…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.
|
Thanks for the careful re-read, @martingarramon. The asymmetry was real — I only noticed once you pointed at Pushed 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 Behavior diff:
Also sharpened Happy to rework if you'd rather keep Re-review progress:
|
martingarramon
left a comment
There was a problem hiding this comment.
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.
Summary
Reported by @awayspam in #77260: in v2026.5.3-1, slash commands like
/newand/statussent in Matrix groups (and other surfaces that emitCommandSource: "text") silently process the action but never deliver the canned confirmation reply whenmessages.groupChat.visibleRepliesis 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
isExplicitSourceReplyCommandand switched the match to theCommandAuthorized: booleansignal thatextensions/matrix/.../handler.ts:1350andextensions/tlon/.../monitor/index.ts:566already populate. After merging that into this branch, the original body-parsingisCommandSourceTurnhelper this PR introduced is no longer needed — the consumer-site bypass atdispatch-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):New (
:315):resolveHarnessSourceVisibleRepliesDefaultonly short-circuits the harnesssourceVisibleReplieslookup whenCommandSource === "native". With a2f1d1d in place, authorized text-prefix slash commands now go throughisExplicitSourceReplyCommandat 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 bareCommandSourcecheck withisExplicitSourceReplyCommandkeeps both short-circuits in sync on a single predicate.Net diff after merge:
src/auto-reply/reply/dispatch-from-config.ts+10/-1— swap:304predicate, add intent commentdocs/channels/groups.md+1/-1— note theCommandAuthorized: truerequirement next to the bypass mentionCHANGELOG.md+1/-0— Unreleased > Fixes entryReal behavior proof
Behavior or issue addressed: Authorized Matrix slash commands like
/newand/status(which arrive asCommandSource: "text"+CommandAuthorized: true) used to fall through to a harness-resolvedmessage_toolsourceVisibleRepliesdefault atdispatch-from-config.ts:306, even though the consumer-siteprefersMessageToolDelivery(now at:729) already recognized them as explicit source-reply commands and disregarded the result. Symptom: a needlessselectAgentHarnessround-trip on every authorized text-slash turn on DM surfaces wheremessages.visibleRepliesis unset. This PR replaces the bareCommandSource === "native"check withisExplicitSourceReplyCommand(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 tomatrix.org. OpenClaw bot configured for a Matrix group room withmessages.groupChat.visibleReplies: "message_tool"and a separate Matrix DM room withmessages.visibleRepliesunset (so the harness default would normally apply). Built locally withpnpm install --frozen-lockfileand started withopenclaw gateway start --config ./test-openclaw.json.Exact steps or command run after this patch:
git checkout 431ec2174d && pnpm install --frozen-lockfile && pnpm buildOPENCLAW_VERBOSE=dispatch openclaw gateway start --config ./test-openclaw.json/newin the group room (withgroupChat.visibleReplies: "message_tool"configured)./statusin the DM room (withmessages.visibleRepliesunset; the configured harness default ismessage_tool).dispatch-from-config: ...for both turns; confirmselectAgentHarnessis 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):
Compared to a build of
upstream/mainat the merge base (db4eea2609) with the same config, the DM/statusturn previously emittedselectAgentHarness:resolve provider=openai model=gpt-4o-minibefore the sameprefersMessageToolDelivery=falsedecision; that line is gone after this patch and the visible reply on both surfaces is unchanged.Observed result after fix: On the group room
/newreturns the canned"✅ New session started."reply visibly (matchesa2f1d1dfd8post-fix behavior at the consumer site, no regression). On the DM room/statusreturns 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 undermessage_tool, as expected).What was not tested: Slack and Mattermost native-slash menu paths;
CommandSource === "native"was already short-circuited before this PR andisExplicitSourceReplyCommandreturns the same value for those turns, so behavior is unchanged by inspection. Tlon was not tested live (no test instance handy); the Tlon adapter atextensions/tlon/.../monitor/index.ts:566populatesCommandAuthorizedthe same way Matrix does, so the predicate's text-slash branch covers it by symmetry.Reproduction
messages.groupChat.visibleReplies: "message_tool"inopenclaw.json./newin a Matrix group."✅ New session started."reply lands in the channel; harness lookup is also skipped on the DM path.Closes #77260.