Skip to content

fix(parse): sweep an orphaned tag-family closer with no opener - #27

Merged
senamakel merged 1 commit into
mainfrom
fix/tagged-orphan-closer
Sep 25, 2026
Merged

senamakel merged 1 commit into
mainfrom
fix/tagged-orphan-closer

Conversation

@graycyrus

Copy link
Copy Markdown
Contributor

Summary

next_opener() never treats a closing marker as an opener — by design, since positional pairing means a closer only ever pairs with an opener that precedes it. But that leaves a real gap: a model that made its actual tool call over a structured/native channel has no <tool_call> opener anywhere in its text at all, yet sometimes still types a habitual </tool_call> in its narrative out of trained habit. With no opener to pair it with, the closer was never recognized as protocol furniture and leaked into the visible chat text verbatim.

Real-world repro

Observed live, mid-answer, right after a "Reasoning · 1 tool call" step in OpenHuman:

Let me check the details on the top contenders to find the best one for you.
</tool_call>
[... normal answer continues ...]

That bare </tool_call> rendered as literal bold text in the chat.

Fix

probe_decided now also checks for a closing tag-family marker that sits before the next real opener (or has no opener after it at all) and sweeps it as Decoded::Noise — the same treatment invoke_xml's WRAPPER_RE already gives a bare wrapper closer.

Also extracted the doubled-opener-skip loop into its own tag_close_skipping_doubled_openers helper: the orphan-closer check pushed probe_decided over clippy's too_many_lines limit, and the loop was a natural, self-contained unit to pull out — no behavior change there, cargo test before and after the extraction is byte-identical output.

Test plan

  • New test an_orphaned_closer_with_no_opener_anywhere_is_removed_not_shown reproduces the exact leaked sequence — fails without the fix, passes with it.
  • cargo test -p tinytools-agent: 323 passed, no regressions (in particular a_doubled_opener_does_not_swallow_an_unrelated_closing_tag and the pipe/newline doubled-closer tests, which exercise the extracted loop, still pass unchanged).
  • cargo fmt --all -- --check: clean.
  • cargo clippy --all-targets --all-features -- -D warnings: clean.
  • cargo build --all-targets --all-features: clean.

next_opener() never treats a closing marker as an opener — by design,
since positional pairing means a closer only ever pairs with an
opener that precedes it. But that leaves a real gap: a model that
made its actual tool call over a structured/native channel has no
`<tool_call>` opener anywhere in its *text* at all, yet sometimes
still types a habitual `</tool_call>` in its narrative out of trained
habit. With no opener to pair it with, the closer was never
recognized as protocol furniture and leaked into the visible chat
text verbatim.

Reproduces a real leak observed live, mid-answer, right after a
"Reasoning · 1 tool call" step:

    Let me check the details on the top contenders to find the best
    one for you.
    </tool_call>
    [... normal answer continues ...]

Fix: probe_decided now also checks for a closing tag-family marker
that sits before the next real opener (or has no opener after it at
all) and sweeps it as Decoded::Noise — the same treatment invoke_xml's
WRAPPER_RE already gives a bare wrapper closer.

Also extracted the doubled-opener-skip loop into its own
`tag_close_skipping_doubled_openers` helper: the orphan-closer check
pushed `probe_decided` over clippy's `too_many_lines` limit, and the
loop was a natural, self-contained unit to pull out — no behavior
change there, `cargo test` before and after the extraction is
byte-identical output.

Test plan:
- New test `an_orphaned_closer_with_no_opener_anywhere_is_removed_not_shown`
  reproduces the exact leaked sequence — fails without the fix, passes
  with it.
- `cargo test -p tinytools-agent`: 323 passed, no regressions (in
  particular `a_doubled_opener_does_not_swallow_an_unrelated_closing_tag`
  and the pipe/newline doubled-closer tests, which exercise the
  extracted loop, still pass unchanged).
- `cargo fmt --all -- --check`: clean.
- `cargo clippy --all-targets --all-features -- -D warnings`: clean.
- `cargo build --all-targets --all-features`: clean.
@tinysweeper

tinysweeper Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Tiny Sweeper review

Tiny Sweeper reviewed this change across 6 lane(s) and found 0 active actionable finding(s). Detailed lane evidence and any incomplete work are listed below.

State: Ready for maintainer review
Priority: none
Reviewed head: 4e93ae666b5d
Updated: 1790334262 (Unix time)

Review snapshot

Change surface Files Review signal Count
Production 1 Active findings 0
Tests 1 Noted findings 0
Documentation 0 Resolved findings 0
Configuration 0 Pending checks/questions 0

Completeness: Complete
Test assessment: No supported feature-to-test mapping was available; this does not mean tests are absent or passed.

What changed

The review could not produce a supported behavioral summary; inspect the cited changed surface and lane details below.

Features

None identified with supported citations.

Tests

No supported feature-to-test mapping was produced. Test execution is not inferred.

Findings

No active actionable findings.

Before merge

None.

How this fits together

flowchart LR
  n0["Tagged<br/>changed"]:::changed
  n1["swallow_extra_closers<br/>changed"]:::changed
  n2["two_adjacent_blocks_are_still_two_blocks<br/>changed"]:::changed
  n3["probe_decided"]:::impacted
  n4["parse_tool_calls"]:::impacted
  n5["Grammar"]:::impacted
  n6["parse"]:::impacted
  n7["len"]:::impacted
  n8["ok"]:::impacted
  n0 -->|implements| n5
  n1 -->|calls| n7
  n2 -->|calls| n6
  n3 -->|calls| n1
  n3 -->|calls| n7
  n3 -->|calls| n8
  n6 -->|calls| n4
  n6 -->|tests| n4
  classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
  classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
  classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
  classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Loading
Agent review details

critique

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: The change correctly removes standalone tagged closers and preserves the existing doubled-opener pairing behavior. I found no correctness issue that blocks merging. _The code index is behind this pull request (indexed at `8128019680df`), so retrieved context may be out of date._ _Memory was unavailable (model: cortex: v1/recall: timed out after 10s), so this review ran without it._

security

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: The tagged-parser changes correctly remove orphaned closing markers and preserve doubled-opener handling without introducing a security or authorization issue. The change is safe to merge. _The code index is behind this pull request (indexed at `8128019680df`), so retrieved context may be out of date._ _Memory was unavailable (model: cortex: v1/recall: timed out after 10s), so this review ran without it._

tests

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: _The code index is behind this pull request (indexed at `8128019680df`), so retrieved context may be out of date._ _Memory was unavailable (model: cortex: v1/recall: timed out after 10s), so this review ran without it._

commits

  • Conclusion: Neutral
  • Scope reviewed: all assigned evidence
  • Lane summary: Nothing sensitive found in what this pull request commits.

description

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: Adds `orphan_closer_noise` to sweep a closing tag-family marker that has no matching opener (or sits before the first opener), preventing literal markup from leaking into visible chat text. Extracts `tag_close_skipping_doubled_openers` to stay under clippy's line limit. Tests pass and cover the exact repro case. _The code index is behind this pull request (indexed at `8128019680df`), so retrieved context may be out of date._ _Memory was unavailable (model: cortex: v1/recall: timed out after 10s), so this review ran without it._

e2e

  • Conclusion: Neutral
  • Scope reviewed: all assigned evidence
  • Lane summary: No end-to-end harness in this repository: no e2e test files and no e2e workflow.
Evidence and run details
  • Models: ladder/vectors, gpt-5.6-luna, deepseek/deepseek-v4-flash
  • Spend: $0.006254
  • Tokens: 106313 input · 13461 output · 3992 cached · 406 embedding
Head State Pass summary
4e93ae666b5d ready for maintainer review 0 active finding(s), 0 resolved finding(s) (at 1790334262)

tinysweeper 0.1.0

@coderabbitai

coderabbitai Bot commented Sep 25, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 32d4ac8d-4927-4655-a695-8df967e169ac


Comment @coderabbitai help to get the list of available commands.

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tinysweeper found nothing blocking. Approving.

             $0.0063 · 106,313 in / 13,461 out · 3,992 cached (4%) · ladder/vectors, gpt-5.6-luna, deepseek/deepseek-v4-flash · 406 embedded
critique:    $0.0012 · 41,644 in  / 2,325 out  · 2,118 cached (5%) · gpt-5.6-luna
security:    $0.0011 · 41,156 in  / 1,295 out  · 1,874 cached (5%) · gpt-5.6-luna
tests:       $0.0016 · 14,160 in  / 1,878 out  · 0 cached (0%)     · deepseek/deepseek-v4-flash
description: $0.0016 · 5,781 in   / 5,949 out  · 0 cached (0%)     · deepseek/deepseek-v4-flash

@senamakel
senamakel merged commit f06fe98 into main Sep 25, 2026
16 checks passed
@senamakel
senamakel deleted the fix/tagged-orphan-closer branch September 30, 2026 06:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants