Skip to content

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

Open
RerankerGuo wants to merge 2 commits into
MemTensor:dev-v2.0.36from
RerankerGuo:fix/issue-2420-clean-json-fence
Open

RerankerGuo wants to merge 2 commits into
MemTensor:dev-v2.0.36from
RerankerGuo:fix/issue-2420-clean-json-fence

Conversation

@RerankerGuo

Copy link
Copy Markdown
Contributor

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

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

Memtensor-AI commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

🤖 Open Code Review

Target: PR #2489
Task: ccaf7534bd494db1
Base: dev-v2.0.36
Head: fix/issue-2420-clean-json-fence

✅ 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

All tests passed (12/12 executed). memos_python_core/changed-repo-python: 12/12. Duration: 13s

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

@RerankerGuo

Copy link
Copy Markdown
Contributor Author

Fixed in 52dfebc.

Build failure. ruff check failed with E402 Module level import not at top of file on src/memos/mem_os/utils/format_utils.py — the two fence regexes had been inserted between the stdlib import block and the memos imports. The constants now sit below all imports.

OpenCodeReview finding 1. The opening-fence info string was the greedy class [^\n] , which swallowed the payload of a single-line fenced response: clean_json_response('{"a": 1}') returned an empty string, and the same happened for a single-line fence carrying an info string followed by a space. The class is now restricted to a real info string ([ \t][A-Za-z0-9_+-][ \t]`), with the newline kept optional so a truncated fence is still stripped. Regression tests for both single-line forms are included.

RerankerGuo and others added 2 commits October 28, 2026 02:52
…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.
@RerankerGuo
RerankerGuo force-pushed the fix/issue-2420-clean-json-fence branch from 52dfebc to 9256d54 Compare October 27, 2026 18:52
@Memtensor-AI

Copy link
Copy Markdown
Collaborator

✅ Automated Test Results: PASSED

All tests passed (14/14 executed). memos_python_core/changed-repo-python: 14/14. Duration: 9s [advisory, non-gating] AI-generated tests on branch test/auto-gen-ccaf7534bd494db1-20261028144521: 163/163 passed — these do NOT affect the PR verdict; review the branch manually.

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

@Memtensor-AI Memtensor-AI added status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发 and removed status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 labels Oct 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core MOS 编排层 / 框架底座 / 跨模块问题 status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants