Skip to content

fix(mem_os): strip only anchored markdown fences in clean_json_response (#2420) - #2590

Open
syzsunshine219 wants to merge 2 commits into
MemTensor:mainfrom
RerankerGuo:fix/issue-2420-clean-json-fence
Open

syzsunshine219 wants to merge 2 commits into
MemTensor:mainfrom
RerankerGuo:fix/issue-2420-clean-json-fence

Conversation

@syzsunshine219

@syzsunshine219 syzsunshine219 commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator

Restores the change proposed in #2489 for review against main after its former base branch, dev-v2.0.36, was deleted. The source commits and authors are preserved.

Source head: 41269f335d7c08a13cc1242eff662ff688203434

This PR is open for review. Review and CI against the current base are pending; validation claims in the original description below refer to the earlier snapshot. The original discussion and evidence remain in #2489.


Original change description

Summary

clean_json_response used a global response.replace("```json", "").replace("```", ""), which removed every triple-backtick sequence — including ones inside JSON string values. Any LLM response whose payload contained a code snippet came out silently corrupted before json.loads.

Root cause

src/memos/mem_os/utils/format_utils.py:1416 (pre-fix):

return response.replace("```json", "").replace("```", "").strip()

Reproducer from the issue:

clean_json_response("```json\n{\"text\": \"```py\\ncode```\"}\n```")
# before:  "{\"text\": \"py\\ncode\"}"      ← fences stripped from the value
# after:   "{\"text\": \"```py\\ncode```\"}"

Both api/handlers/chat_handler.py and api/handlers/suggestion_handler.py pipe responses through this function, so the bug surface was the entire chat-metadata and suggestion pipeline.

Fix

Replace the global string replace with two anchored regex substitutions that only strip the outer fences:

  • Opening: re.compile(r"^```[^\n`]*\n?") — covers the optional info string (json or otherwise) and trailing newline
  • Closing: re.compile(r"\n?```\s*$") — at the very end only

Interior ``` sequences inside a JSON string value are preserved verbatim. As a side effect the function now correctly handles responses that have only an opening or only a closing fence (the original implementation would leave those stranded mid-string).

Tests

Added four cases to tests/mem_os/test_format_utils.py:

  1. test_clean_json_response_preserves_internal_fenced_code — the issue reproducer (red against unpatched main)
  2. test_clean_json_response_preserves_unclosed_fence_in_value — a value containing a single unclosed fence
  3. test_clean_json_response_handles_only_opening_fence — best-effort handling when the closing fence is missing
  4. test_clean_json_response_handles_only_closing_fence — best-effort handling when the opening fence is missing

Existing tests (single backticks in JSON content, plain JSON, complex JSON, etc.) all continue to pass — test_clean_json_response_preserves_internal_backticks was a good seed for the new tests.

Verification

PYTHONPATH=src pytest tests/mem_os/test_format_utils.py -v
============================== 12 passed in 3.09s ==============================

Ruff: ruff format --check and ruff check both clean on touched files (the 2 pre-existing findings were auto-fixed by ruff check --fix during this PR — both were E501 line-too-long in clean_json_response itself, not introduced by this change).

Out of scope

  • No new dependencies
  • No public API / OpenAPI contract changes
  • No DB schema, migration, or graph model changes

Closes #2420

RerankerGuo and others added 2 commits November 4, 2026 21:58
…se (MemTensor#2420)

clean_json_response previously used a global
`response.replace("```json", "").replace("```", "")`, which removed
every triple-backtick sequence in the response — including ones that lived
inside JSON string values. Any LLM response whose payload contained a code
snippet (a fenced Python block, an unclosed partial fence, etc.) came out
silently corrupted before json.loads, so suggestions and chat metadata
carrying markdown snippets were returned to callers with mangled content.

Both api/handlers/chat_handler.py and api/handlers/suggestion_handler.py
pipe responses through clean_json_response, so the bug surface was the
entire suggestion and chat pipeline.

The fix replaces the global string replace with two anchored regex
substitutions that only strip the outer fences:

- opening fence (^\`\`\`[^\\n`]*\\n?)  — covers the optional
  info string (`json` or otherwise) and trailing newline
- closing fence (\\n?\`\`\`\\s*$) — at the very end only

Interior \`\`\` sequences inside a JSON string value are preserved
verbatim. As a side effect the function now correctly handles responses
that have only an opening or only a closing fence (the original
implementation would leave those stranded mid-string).

- Add module-level _JSON_FENCE_OPEN_RE / _JSON_FENCE_CLOSE_RE
- Rewrite clean_json_response body to use anchored re.sub
- Four new tests in tests/mem_os/test_format_utils.py pin the contract:
  the issue reproducer (interior fenced code preserved), unclosed-fence-in-value,
  opening-only response, and closing-only response

Fixes MemTensor#2420.
… pattern

Move the fence regex constants below the memos imports so ruff's E402
(module-import-not-at-top) passes again; the constants had been inserted
between the stdlib and memos import blocks, failing every build-matrix
job at the Ruff checks step.

Restrict the opening-fence info string from the greedy not-newline-not-
backtick class to a blank-padded short identifier. The greedy class
matched JSON payload characters up to the next backtick, so a
single-line fenced payload — triple-backtick + JSON + triple-backtick
with no newline after the fence, or with an info string followed by a
space — was swallowed whole and clean_json_response returned an empty
string, breaking json.loads downstream. The tightened class only accepts
an actual info string and keeps the newline optional so truncated fences
are still stripped.

Add regression tests for both single-line fenced forms.
@Memtensor-AI Memtensor-AI added area:core MOS 编排层 / 框架底座 / 跨模块问题 status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 labels Nov 9, 2026
@syzsunshine219
syzsunshine219 marked this pull request as ready for review November 9, 2026 15:48
@Memtensor-AI

Copy link
Copy Markdown
Collaborator

🤖 Open Code Review

Target: PR #2590
Task: e45aa70eef2d46db
Base: main
Head: fix/issue-2420-clean-json-fence
Head SHA: 41269f335d7c08a13cc1242eff662ff688203434

✅ OpenCodeReview: Review complete: 0 finding(s) across 1 selected item(s).

Generated by cloud-assistant via Open Code Review.

@Memtensor-AI

Copy link
Copy Markdown
Collaborator

✅ Automated Test Results: PASSED

Required tests passed (14/14 executed, 0 skipped). Duration: 17s [advisory, non-gating] AI-generated tests on branch test/auto-gen-e45aa70eef2d46db-20261110001310: 54/54 passed — these do NOT affect the PR verdict; review the branch manually.

Branch: fix/issue-2420-clean-json-fence

Tested source: 41269f335d7c08a13cc1242eff662ff688203434
Tested base: 3a16e5fe3048a209f722806f4123fa2e533588fd
Tested commit: 6536557b4f9a55d80c8078e0f61b7f9504580d74
These results apply only to this immutable source/base snapshot.

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

Labels

area:core MOS 编排层 / 框架底座 / 跨模块问题 status:in-progress Someone or AI is working on it | 人工或 AI 正在处理

Projects

None yet

Development

Successfully merging this pull request may close these issues.

clean_json_response corrupts JSON values containing code fences

4 participants