Skip to content

fix(loop): strip model-emitted render tags to break #206 echo feedback loop - #228

Merged
ranxianglei merged 2 commits into
masterfrom
2026-08-25_fix-206-tag-echo-loop
Aug 25, 2026
Merged

ranxianglei merged 2 commits into
masterfrom
2026-08-25_fix-206-tag-echo-loop

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

Problem (#206)

Models sometimes imitate the `mNNNNN` render tags that compressed history is rendered into, emitting them in visible output ("tag echo"). The client stores the echoed tags in its local history and replays them next turn; the model then sees its own echoes and repeats further — feedback amplification, occasionally unbounded repetition.

The proxy already logged tag echoes (3 warn sites) but never mitigated them.

Fix

Strip render tags from outgoing model text at the wire adapters, breaking the loop at the source:

  • new `src/loop/tag-echo-filter.ts` — streaming-safe filter (hold/partial-tag handling, swallow-until-close, truncated-tag flush) + one-shot `stripAcpTags` + Responses-object `stripResponsesText`.
  • `adapter-anthropic.ts` / `adapter-openai.ts` / `adapter-responses.ts`: filter text deltas in `parseStream`, flush tails at stream end, strip full-text fields on `output_text.done` / `output_item.done` / `response.completed`; raw SSE chunks are rewritten so round-1 passthrough is clean too.
  • Non-streaming paths: `rewriteJsonResponse` (anthropic), `rewriteOpenaiJsonResponse` (openai), `compress-loop-responses.ts` (responses JSON).
  • One `[warn] [tag-echo]` log per response (not per delta).

Safety

  • Only the render form is stripped (`<acp `…`>` / exact ``); underscore-namespaced text-protocol triggers (`<acp_compress>` etc.) and ordinary prose containing `<` pass through untouched.
  • Long (>64 chars) paired content keeps its content, only tags are removed.

Tests

`tests/tag-echo.test.ts` — 11 tests: unit stripping semantics, exhaustive streaming-vs-batch equivalence (every split position + pseudo-random chunkings, incl. the 21×-repeated-tag shape from the #206 report), and end-to-end adapter tests for all three protocols across split deltas. Full suite: 576/576 pass; typecheck + build clean.

…k loop

Models sometimes imitate the render tags injected into compressed history
and emit them in visible output. The client stores the echoed tags, replays
them next turn, and the imitation amplifies into unbounded repetition (#206).

Strip render tags from outgoing text in all three wire adapters (streaming
and non-streaming) with a streaming-safe filter; underscore-namespaced
text-protocol triggers and ordinary prose pass through untouched.
@github-actions

github-actions Bot commented Aug 24, 2026 •

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-08-25_fix-206-tag-echo-loop (2cc9e3c)

Option A — Install from npm PR tag (recommended)

npm install -g billion-context@pr-228

Each push to this PR publishes a new version under the pr-228 npm tag.

Option B — Download artifact

  1. Download the artifact from the Actions run
  2. Extract the tarball and install:
tar xzf billion-context-pr228.tgz
npm install -g package

This comment is automatically updated on each push.

- fix filter false-swallow after paired tags (wasPaired check)
- containsRenderTagText: detect JSON-escaped open tag with attrs
- non-stream gates use containsRenderTagText (catch bare lone tags)
- openai rewriteContentChunk preserves sibling choices
- add e2e tests (real server, compress round-trip) and non-stream tests
@ranxianglei
ranxianglei merged commit 1f4a578 into master Aug 25, 2026
6 checks passed
@ranxianglei
ranxianglei deleted the 2026-08-25_fix-206-tag-echo-loop branch October 15, 2026 05:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant