Skip to content

[bug] #644 follow-up (post-#646, v0.1.97): typo'd render-tag echo (<acip …) bypasses the strip regex and passes through as a 43-char degenerate end_turn turn — agent stalls mid-orchestration until manual nudge #673

Description

@chenshuangci

Summary

Follow-up of #644, observed on v0.1.97 which already includes the #646 fix. A new escape form: the model emits a typo'd render-tag echo<acip tokens="36" type="text">m00473</acip> (acip, not acp). None of the tag-echo strip regexes match it, so the malformed fragment is passed through verbatim as the turn's entire text (43 chars), with stop_reason="end_turn" and zero tool_use, while the thinking block still contains a complete next-step plan. The agent records a degenerate turn as a normal completion and stalls mid-orchestration until a manual nudge.

Two gaps:

  1. Strip requires exact <acp spelling — a one-letter typo structurally bypasses every defense added in fix: bound close-side tag-echo tail so a malformed </acp can't eat following content (#644) #646.
  2. No degenerate-turn detection — post-strip text empty or a pure echo fragment + end_turn + zero tool_use + non-empty thinking is treated as a normal completed turn by the model, bili, and the host. Nobody catches it.

Evidence chain (three-way closure, all UTC 2026-09-09)

1. Upstream raw SSE dump ~/.local/share/billion-context/raw/1788980229387-0166b4ba-afc8-4d0b-92b0-a8f599f5bcbd-raw.sse (30,341 bytes / 226 events):

  • All text deltas concatenated = exactly the 43-char fragment <acip tokens="36" type="text">m00473</acip> — this is the turn's entire visible output.
  • message_delta carries genuine stop_reason="end_turn" with usage input=419, cache_read=115584, output=223 (ruling out the emitError fabrication path, which has an [acp-proxy: ...] text prefix and all-zero usage).
  • Zero tool_use content blocks.
  • The thinking block (386 chars) ends with a complete next-step plan — the model degenerated mid-orchestration (thinking–act disconnect), it did not deliberately finish.

2. bili.log: last acp-usage for the session at 18:57:13.762Z (input=116003 = 419+115584, closes with the dump), then zero session activity until 18:59:09Z when the user manually sends "continue" (processTurn: 267 msgs resumes normally). The string acip has 0 matches in the entire log — the strip never saw the fragment. The 6 [tag-echo] stripped lines in the surrounding window (18:55–18:58Z) are all exact-spelling <acp from other turns.

3. Host side (client log): the turn is recorded as a normal completion — responseLength=43, toolCallCount=0, finishReason="stop", followed by turn.completed, no error events.

The echoed ref m00473 is the message id the user later quoted when reporting the stall — confirming the stream belongs to the affected session.

Why current defenses miss

Strip regexes in dist/index.js (~offset 2223722):

PAIRED     = /<acp\s[^<>]{0,256}>([^<>]{0,64})<\/acp>/
LONE_OPEN  = /<acp(?:\s[^<>]{0,256})?>/
TRUNC_OPEN = /<acp\s[^<>]*$/

All require the exact <acp opening; the string acip appears 0 times in the whole bundle. The typo'd tag is never matched, so #646's close-side bound handling never engages — the fragment rides through untouched.

The typo is chronic, not a one-off

Later dumps from the same day show the variant repeatedly, e.g. one turn mixing a correct <acp opening with a typo'd </acip closing (dump 1788980392406). Variant tolerance matters beyond a single fluke.

Impact

  • Mid-orchestration stall until a human nudge (~2 min here; the same family has caused multi-hour silent gaps).
  • The 43-char garbage fragment is shown to the user as the turn's report.

Suggestions

  1. Echo detection should not depend on exact spelling — e.g. tolerate close variants of the tag name (<acp/<acip-ish shapes wrapping an acp-style tokens=/ref payload), scoped tightly so legit text is not eaten.
  2. Degenerate-turn detection: after stripping, if the final text is empty or a pure echo fragment AND stop_reason=end_turn AND zero tool_use AND the thinking block is non-empty → emit a warning (or auto-retry the turn once). Currently all three layers treat it as a normal completion, which is what turns a model glitch into a silent stall.

Environment: bili v0.1.97 (includes #646, shipped in v0.1.96), GLM-5.3 upstream, dumps under ~/.local/share/billion-context/raw/.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions