Skip to content

fix(telegram): suppress JSON-wrapped NO_REPLY leaking to end users#37891

Closed
Clawborn wants to merge 2 commits intoopenclaw:mainfrom
Clawborn:fix/suppress-json-no-reply-leak
Closed

fix(telegram): suppress JSON-wrapped NO_REPLY leaking to end users#37891
Clawborn wants to merge 2 commits intoopenclaw:mainfrom
Clawborn:fix/suppress-json-no-reply-leak

Conversation

@Clawborn
Copy link
Copy Markdown
Contributor

@Clawborn Clawborn commented Mar 6, 2026

Problem

When a model emits {"action":"NO_REPLY"} instead of the bare NO_REPLY sentinel, the outbound pipeline fails to recognise it as a silent reply, causing raw JSON to be delivered as a visible Telegram message.

Fix

Extend isSilentReplyText with a isJsonWrappedSilentToken helper that detects small JSON objects (≤2 keys, ≤200 chars) whose value matches the silent token. Safe against false positives on larger payloads.

Tests

6 new test cases covering exact match, whitespace variants, case insensitivity, and negative cases.

Closes #37727

Clawborn added 2 commits March 6, 2026 21:29
decodeURIComponent throws on invalid sequences like %ZZ. Without a
try-catch the canvas file-resolver would propagate an unhandled
URIError, crashing the request. Fall back to the raw path on failure.
…penclaw#37727)

When a model emits `{"action":"NO_REPLY"}` instead of the bare
`NO_REPLY` sentinel, the outbound pipeline previously failed to
recognise it as a silent reply, causing the raw JSON to be delivered
as a visible Telegram message.

Extend `isSilentReplyText` to detect small JSON objects whose sole
meaningful value equals the silent token. Add regression tests.

Closes openclaw#37727
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 6, 2026

Greptile Summary

This PR fixes two independent issues: (1) JSON-wrapped NO_REPLY sentinels (e.g. {"action":"NO_REPLY"}) leaking as visible Telegram messages, and (2) a crash in normalizeUrlPath when given malformed percent-encoded URL paths.

The file-resolver.ts fix is clean and safe — the try/catch around decodeURIComponent correctly falls back to the raw path, and path.posix.normalize does not decode percent-encoding, so no new traversal surface is introduced.

The tokens.ts fix has a logic gap worth addressing:

  • False-positive suppression for 2-key objects: The docstring states only objects where "the sole meaningful value equals the silent token" should be suppressed, but values.some(...) also matches 2-key objects where just one value is the token. A model response like {"action":"NO_REPLY","content":"Goodbye!"} would be silently dropped even though it carries real user content in the second key.
  • Missing test coverage: The test suite does not include the 2-key mixed-value case, leaving this behaviour undocumented and the discrepancy undetected.

Confidence Score: 3/5

  • Mergeable after the 2-key false-positive logic in isJsonWrappedSilentToken is resolved.
  • The file-resolver.ts change is safe and well-contained. The core tokens.ts logic works correctly for the 1-key case (the most common model behaviour). However, the 2-key path contradicts the documented intent and could silently suppress legitimate reply content, which is a real correctness concern — not just a style nit. The fix is small (change some to every, or restrict to 1-key only), but until it is made the implementation doesn't match its contract.
  • src/auto-reply/tokens.ts lines 114–120 (isJsonWrappedSilentToken 2-key guard) and the corresponding test file.

Last reviewed commit: 84121ea

Comment on lines +114 to +120
// Accept objects with 1–2 keys where at least one value is the silent token.
if (values.length === 0 || values.length > 2) {
return false;
}
return values.some(
(v) => typeof v === "string" && v.trim().toUpperCase() === token.toUpperCase(),
);
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.

2-key object allows false-positive suppression

The docstring says "the sole meaningful value equals the silent token", but the implementation accepts objects with up to 2 keys where any value matches — values.some(...). This means a model response like {"action":"NO_REPLY","content":"Goodbye, have a great day!"} would be silently swallowed even though it carries real user-visible content in "content".

The intent (and the docstring) appears to be that the only non-null value in the object should be the sentinel. Either the key-count guard should be reduced to values.length > 1 (i.e., allow only single-key objects), or some should be replaced with every so that all values must equal the token. As written, the 2-key path contradicts the stated contract.

// Option A – restrict to single-key objects only (matches docstring intent)
if (values.length === 0 || values.length > 1) {
  return false;
}
return (
  typeof values[0] === "string" &&
  values[0].trim().toUpperCase() === token.toUpperCase()
);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/auto-reply/tokens.ts
Line: 114-120

Comment:
**2-key object allows false-positive suppression**

The docstring says "the sole meaningful value equals the silent token", but the implementation accepts objects with up to 2 keys where **any** value matches — `values.some(...)`. This means a model response like `{"action":"NO_REPLY","content":"Goodbye, have a great day!"}` would be silently swallowed even though it carries real user-visible content in `"content"`.

The intent (and the docstring) appears to be that the **only** non-null value in the object should be the sentinel. Either the key-count guard should be reduced to `values.length > 1` (i.e., allow only single-key objects), or `some` should be replaced with `every` so that all values must equal the token. As written, the 2-key path contradicts the stated contract.

```typescript
// Option A – restrict to single-key objects only (matches docstring intent)
if (values.length === 0 || values.length > 1) {
  return false;
}
return (
  typeof values[0] === "string" &&
  values[0].trim().toUpperCase() === token.toUpperCase()
);
```

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

Comment on lines +119 to +125
it("does not suppress objects with unrelated values", () => {
expect(isSilentReplyText('{"action":"hello"}')).toBe(false);
});

it("does not suppress larger objects", () => {
expect(isSilentReplyText('{"action":"NO_REPLY","text":"hi","extra":"data"}')).toBe(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.

Missing test for 2-key mixed-value object

The test suite covers a 1-key non-matching object ({"action":"hello"}) and a 3-key object, but there is no test for the critical 2-key case where one value is the sentinel and the other is unrelated content. Due to the values.some(...) logic, {"action":"NO_REPLY","content":"Hello!"} currently returns true (suppressed). A test documenting the intended behaviour here would catch the discrepancy:

it("does not suppress 2-key objects where only one value is the token", () => {
  expect(isSilentReplyText('{"action":"NO_REPLY","content":"Hello!"}')).toBe(false);
});
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/auto-reply/tokens.test.ts
Line: 119-125

Comment:
**Missing test for 2-key mixed-value object**

The test suite covers a 1-key non-matching object (`{"action":"hello"}`) and a 3-key object, but there is no test for the critical 2-key case where one value is the sentinel and the other is unrelated content. Due to the `values.some(...)` logic, `{"action":"NO_REPLY","content":"Hello!"}` currently returns `true` (suppressed). A test documenting the intended behaviour here would catch the discrepancy:

```typescript
it("does not suppress 2-key objects where only one value is the token", () => {
  expect(isSilentReplyText('{"action":"NO_REPLY","content":"Hello!"}')).toBe(false);
});
```

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

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: 84121ea650

ℹ️ 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 +115 to +119
if (values.length === 0 || values.length > 2) {
return false;
}
return values.some(
(v) => typeof v === "string" && v.trim().toUpperCase() === token.toUpperCase(),
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 Avoid silencing two-field JSON replies with extra content

This matcher suppresses any 1–2 key JSON object when any value equals NO_REPLY, which means legitimate structured replies can now be dropped if they include that string in one field (for example {"status":"NO_REPLY","message":"done"}); because isSilentReplyText is used as a suppression gate in outbound reply paths, this introduces silent message loss for valid JSON outputs rather than just handling the token-only sentinel payload.

Useful? React with 👍 / 👎.

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

2 participants