Skip to content

fix(reply): suppress JSON/channelData NO_REPLY action payloads#37904

Open
kaiwangleo wants to merge 1 commit intoopenclaw:mainfrom
kaiwangleo:fix/37727-no-reply-action-guard
Open

fix(reply): suppress JSON/channelData NO_REPLY action payloads#37904
kaiwangleo wants to merge 1 commit intoopenclaw:mainfrom
kaiwangleo:fix/37727-no-reply-action-guard

Conversation

@kaiwangleo
Copy link
Copy Markdown

@kaiwangleo kaiwangleo commented Mar 6, 2026

Summary

  • treat JSON action-only payload text like {"action":"NO_REPLY"} as silent internal control payloads
  • suppress channelData-only payloads that carry action: NO_REPLY
  • keep media behavior unchanged (media + NO_REPLY action still allowed)
  • add regression tests for JSON action form and channelData action form

Why

Issue #37727 reports occasional leakage of internal NO_REPLY action payloads into Telegram user-visible messages.

Validation

  • pnpm -s test src/auto-reply/reply/reply-utils.test.ts

Fixes #37727

Copy link
Copy Markdown

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

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: 3dee023e04

ℹ️ 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".

}
try {
const parsed = JSON.parse(trimmed) as Record<string, unknown>;
return isNoReplyActionValue(parsed.action);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restrict NO_REPLY JSON suppression to action-only payloads

parseNoReplyActionJsonText marks any JSON text with action: "NO_REPLY" as silent, and the subsequent guard in normalizeReplyPayload drops the reply whenever there is no media or effective channelData. That means legitimate JSON responses such as {"action":"NO_REPLY","note":"example"} are fully suppressed even though they are not action-only control payloads, so users can receive no message when asking for JSON content that happens to include this field.

Useful? React with 👍 / 👎.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 6, 2026

Greptile Summary

This PR adds suppression logic for two new forms of NO_REPLY control payloads that were leaking into user-visible Telegram messages: JSON text payloads ({"action":"NO_REPLY"}) and channelData-only payloads carrying an action: "NO_REPLY" key. The approach is generally correct for the pure-control-payload cases, but there are two bugs worth fixing before merging:

  • Custom silentToken not respected — the new helper functions (isNoReplyActionValue, parseNoReplyActionJsonText, containsNoReplyAction) hardcode SILENT_REPLY_TOKEN rather than accepting the effective token. normalizeReplyPayload already resolves opts.silentToken ?? SILENT_REPLY_TOKEN for all existing checks; the new checks should do the same.

  • JSON action text is not cleared when media is present — when text = '{"action":"NO_REPLY"}' and a mediaUrl is also present, both early-return guards are correctly skipped, but the JSON string then falls through to stripSilentToken. That regex ((?:^|\s+|\*+)NO_REPLY\s*$) requires the token to be preceded by start-of-string, whitespace, or asterisks; the " character before NO_REPLY in the JSON does not match, so the string is returned unchanged. The new test for this case only asserts on mediaUrl, missing the text leak entirely.

Confidence Score: 2/5

  • Not safe to merge as-is — two logic bugs allow the JSON action payload to remain user-visible in the media case, and the configurable silentToken option is silently ignored by the new suppression paths.
  • The pure-suppression paths (no media, no real text) work correctly and the first two regression tests are solid. However, the media + JSON action case has a real text leak that the test does not catch, and the custom silentToken inconsistency could hide bugs for any caller relying on that option.
  • src/auto-reply/reply/normalize-reply.ts (helper functions need token parameterization and text-clearing when textIsNoReplyActionJson && hasMedia) and src/auto-reply/reply/reply-utils.test.ts (third new test needs a result.text assertion).

Last reviewed commit: 3dee023

Comment on lines +174 to +181
it("keeps JSON action payload when media exists", () => {
const result = normalizeReplyPayload({
text: '{"action":"NO_REPLY"}',
mediaUrl: "https://example.com/img.png",
});
expect(result).not.toBeNull();
expect(result!.mediaUrl).toBe("https://example.com/img.png");
});
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.

Test incomplete — JSON text leaks into media payload

This test only asserts that result.mediaUrl survives, but the raw JSON string '{"action":"NO_REPLY"}' is also present in result.text and would be sent to the user.

Trace of the media-present path for text = '{"action":"NO_REPLY"}':

  1. Early returns at lines 72–79 are skipped because hasMedia = true.
  2. isSilentReplyText('{"action":"NO_REPLY"}', "NO_REPLY")false (not an exact-match).
  3. text.includes("NO_REPLY")true, so stripSilentToken is called.
  4. getSilentTrailingRegex produces (?:^|\s+|\*+)NO_REPLY\s*$. In '{"action":"NO_REPLY"}' the token is preceded by ", so the regex does not match and the text is returned unchanged.
  5. The function returns { text: '{"action":"NO_REPLY"}', mediaUrl: '...' }.

The test should also assert that the leaked JSON is absent:

Suggested change
it("keeps JSON action payload when media exists", () => {
const result = normalizeReplyPayload({
text: '{"action":"NO_REPLY"}',
mediaUrl: "https://example.com/img.png",
});
expect(result).not.toBeNull();
expect(result!.mediaUrl).toBe("https://example.com/img.png");
});
it("keeps JSON action payload when media exists", () => {
const result = normalizeReplyPayload({
text: '{"action":"NO_REPLY"}',
mediaUrl: "https://example.com/img.png",
});
expect(result).not.toBeNull();
expect(result!.mediaUrl).toBe("https://example.com/img.png");
expect(result!.text).toBe("");
});

Beyond the test, normalize-reply.ts also needs to clear text when textIsNoReplyActionJson is true and the function does not return early (e.g. media is present), to prevent the JSON string from reaching the user.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/auto-reply/reply/reply-utils.test.ts
Line: 174-181

Comment:
**Test incomplete — JSON text leaks into media payload**

This test only asserts that `result.mediaUrl` survives, but the raw JSON string `'{"action":"NO_REPLY"}'` is also present in `result.text` and would be sent to the user.

Trace of the media-present path for `text = '{"action":"NO_REPLY"}'`:
1. Early returns at lines 72–79 are skipped because `hasMedia = true`.
2. `isSilentReplyText('{"action":"NO_REPLY"}', "NO_REPLY")``false` (not an exact-match).
3. `text.includes("NO_REPLY")``true`, so `stripSilentToken` is called.
4. `getSilentTrailingRegex` produces `(?:^|\s+|\*+)NO_REPLY\s*$`. In `'{"action":"NO_REPLY"}'` the token is preceded by `"`, so the regex **does not match** and the text is returned unchanged.
5. The function returns `{ text: '{"action":"NO_REPLY"}', mediaUrl: '...' }`.

The test should also assert that the leaked JSON is absent:

```suggestion
  it("keeps JSON action payload when media exists", () => {
    const result = normalizeReplyPayload({
      text: '{"action":"NO_REPLY"}',
      mediaUrl: "https://example.com/img.png",
    });
    expect(result).not.toBeNull();
    expect(result!.mediaUrl).toBe("https://example.com/img.png");
    expect(result!.text).toBe("");
  });
```

Beyond the test, `normalize-reply.ts` also needs to clear `text` when `textIsNoReplyActionJson` is true and the function does not return early (e.g. media is present), to prevent the JSON string from reaching the user.

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

Comment on lines +28 to +29
function isNoReplyActionValue(value: unknown): boolean {
return typeof value === "string" && value.trim().toUpperCase() === SILENT_REPLY_TOKEN;
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.

opts.silentToken ignored in new helper functions

isNoReplyActionValue (and therefore parseNoReplyActionJsonText and containsNoReplyAction) always compares against the hardcoded SILENT_REPLY_TOKEN constant. However, normalizeReplyPayload supports a configurable silentToken via opts.silentToken ?? SILENT_REPLY_TOKEN (resolved at line 86). Any caller that passes a custom silentToken will find the new JSON and channelData suppression paths silently ignore their override — inconsistent with how all the pre-existing token checks in this function behave.

The helpers should accept the effective token as a parameter, and normalizeReplyPayload should resolve opts.silentToken ?? SILENT_REPLY_TOKEN before the new early-return checks and thread it through every call site.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/auto-reply/reply/normalize-reply.ts
Line: 28-29

Comment:
**`opts.silentToken` ignored in new helper functions**

`isNoReplyActionValue` (and therefore `parseNoReplyActionJsonText` and `containsNoReplyAction`) always compares against the hardcoded `SILENT_REPLY_TOKEN` constant. However, `normalizeReplyPayload` supports a configurable `silentToken` via `opts.silentToken ?? SILENT_REPLY_TOKEN` (resolved at line 86). Any caller that passes a custom `silentToken` will find the new JSON and `channelData` suppression paths silently ignore their override — inconsistent with how all the pre-existing token checks in this function behave.

The helpers should accept the effective token as a parameter, and `normalizeReplyPayload` should resolve `opts.silentToken ?? SILENT_REPLY_TOKEN` **before** the new early-return checks and thread it through every call site.

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

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Telegram: occasional raw {"action":"NO_REPLY"} payload is delivered to end user instead of being suppressed

1 participant