Skip to content

Commit 39b74de

Browse files
mios-devclaude
andcommitted
lazy-open <details> + "Hey there!" skip-refine (fix orphan-tag leak)
Operator-flagged 2026-05-18 chat: status emits ended at "🧠 → hermes" and the raw stream below it showed the open <details type="reasoning" data-mios-agent="hermes"> tag ORPHANED -- no close, no answer body. Two root causes: 1. EAGER <details> open. The pipe yielded the open tag IMMEDIATELY after dispatching to hermes, before any chunks arrived. If hermes was cold-loading a model (30-90s) or returned empty, the operator saw the open tag alone. The empty-output path had a ∅ emit but no close because POLISH_ENABLED gated whether the close was emitted, and the close path only fired AFTER the stream loop -- a hung stream never reached it. Fix: LAZY-OPEN. The pipe tracks _details_opened state. The <details> opener is emitted only on the FIRST chunk that actually carries content (`text_piece` non-empty in delta). Every close path (success, empty-output, timeout, exception) now checks `_details_opened` -- emits the close only if a matching open was emitted. Pure idempotent state machine; no orphan tags possible. 2. "Hey there!" was inflating from 10c -> 111c through refine because _CONVERSATIONAL_RE matched single greetings (`hi`, `hello`, `hey`) but not greeting + neutral salutation ("hey there", "hi y'all", "hello everyone"). The cold model load on this turn would have surfaced #1 anyway, but the conversational gate should have short-circuited refine first and gone straight to hermes for a one-line conversational reply. Fix: extend the leading-word alternation to bundle the salutation form: (hi|hello|hey|yo|howdy)(?:\s+(there|y'all|everyone|all|guys |friend|friends|bot))? Unit-tested 16/16: every greeting+salutation matches, every non-greeting (e.g. "Hey there can you launch notepad", "Hello world, list my apps") correctly DOES NOT match -- so multi- intent prompts that lead with a greeting still go through refine. mios-owui-install-pipe re-ran; OWUI db function.content carries both fixes. Live verified: pipe AST OK; 16/16 conversational-regex tests pass in-container. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent e5ac5bf commit 39b74de

1 file changed

Lines changed: 37 additions & 20 deletions

File tree

usr/share/mios/owui/pipes/mios_agent_pipe.py

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -372,8 +372,14 @@ async def _tail_watcher(
372372
# the alternation. The bare-name list (hi, hola, etc.) covers
373373
# standalone tokens; the phrase list covers multi-word openers.
374374
_CONVERSATIONAL_RE = re.compile(
375-
# English / generic
376-
r"^\s*(hi|hello|hey|yo|howdy|sup|gm|gn|ok|okay|kk|alright|"
375+
# English / generic. The "X there"/"X y'all"/"X everyone"
376+
# forms (Hey there!, Hi y'all, Hello everyone) are bundled
377+
# in the leading-word alternation so they also short-circuit
378+
# to skip-refine. Operator-flagged 2026-05-18: "Hey there!"
379+
# was inflating from 10c to 111c via refine because the
380+
# 1-word gate failed to match.
381+
r"^\s*((?:hi|hello|hey|yo|howdy)(?:\s+(?:there|y[’']?all|everyone|all|guys|friend|friends|bot))?|"
382+
r"sup|gm|gn|ok|okay|kk|alright|"
377383
r"thanks|thx|ty|thank you|cool|nice|great|got it|"
378384
r"sounds good|sgtm|sure|yes|no|yep|nope|yeah|nah|"
379385
r"bye|cya|goodbye|later|peace|seeya|"
@@ -1559,13 +1565,19 @@ async def pipe(
15591565
# appropriate final answer normally in OWUI chats".
15601566
raw_buffer = ""
15611567
any_text = False
1562-
1563-
# Open the collapsible thinking block. OWUI renders
1564-
# <details type="reasoning"> as a click-to-expand row above the
1565-
# assistant message. The streaming chunks land INSIDE so the
1566-
# operator can watch it tick in real time if they expand it.
1567-
if self.valves.POLISH_ENABLED:
1568-
yield (
1568+
# Operator-flagged 2026-05-18: open `<details>` was being
1569+
# yielded EAGERLY before hermes responded -- if hermes was
1570+
# cold-loading a model (30-90s) the operator saw the open tag
1571+
# alone, and if hermes returned empty, the close was missed
1572+
# and OWUI rendered the rest of the message as
1573+
# collapsed-reasoning. Fix: lazy-open. We only emit the open
1574+
# tag the first time a real chunk arrives. _details_opened
1575+
# tracks state so the close path knows whether to emit a
1576+
# matching close.
1577+
_details_opened = False
1578+
1579+
def _open_details_chunk() -> str:
1580+
return (
15691581
f"<details type=\"reasoning\" data-mios-agent=\"hermes\">\n"
15701582
f"<summary>{self.valves.AGENT_THINKING_LABEL}</summary>\n\n"
15711583
)
@@ -1576,8 +1588,7 @@ async def pipe(
15761588
data=json.dumps(body).encode()) as resp:
15771589
if resp.status != 200:
15781590
err = (await resp.text())[:300]
1579-
if self.valves.POLISH_ENABLED:
1580-
yield "</details>\n\n"
1591+
# Nothing to close yet -- we never opened.
15811592
await self._emit(__event_emitter__,
15821593
f"❌ backend {resp.status}: {err}",
15831594
done=True)
@@ -1604,13 +1615,18 @@ async def pipe(
16041615
continue
16051616
any_text = True
16061617
raw_buffer += text_piece
1607-
# Stream into the details block AS IT ARRIVES so
1608-
# the expand-while-streaming UX still works.
1618+
# Lazy-open: emit the <details> opener only
1619+
# when the FIRST chunk arrives.
1620+
if self.valves.POLISH_ENABLED and not _details_opened:
1621+
yield _open_details_chunk()
1622+
_details_opened = True
16091623
yield text_piece
16101624

1611-
# Close the thinking block before polish so the polished
1612-
# answer renders below it as the visible assistant message.
1613-
if self.valves.POLISH_ENABLED:
1625+
# Close the thinking block before polish, BUT only if we
1626+
# actually opened it. An empty-output turn (hermes never
1627+
# streamed) never opens, so never closes -- the operator
1628+
# sees the empty marker below without any orphaned tag.
1629+
if _details_opened:
16141630
yield "\n</details>\n\n"
16151631

16161632
raw_text = raw_buffer.strip()
@@ -1646,16 +1662,17 @@ async def pipe(
16461662

16471663
await self._emit(__event_emitter__, "✅", done=True)
16481664
except asyncio.TimeoutError:
1649-
# Close the <details> if we opened one, otherwise OWUI
1650-
# renders the rest of the message as collapsed-reasoning.
1651-
if self.valves.POLISH_ENABLED:
1665+
# Close the <details> ONLY if lazy-open actually fired.
1666+
# No tag was emitted on empty-stream / cold-load timeouts
1667+
# so we don't orphan a close either.
1668+
if _details_opened:
16521669
yield "\n</details>\n\n"
16531670
await self._emit(__event_emitter__,
16541671
f"⏱️ {self.valves.TIMEOUT_S}s",
16551672
done=True)
16561673
yield f"\n\n_⏱️ {self.valves.TIMEOUT_S}s_"
16571674
except Exception as e:
1658-
if self.valves.POLISH_ENABLED:
1675+
if _details_opened:
16591676
yield "\n</details>\n\n"
16601677
await self._emit(__event_emitter__,
16611678
f"❌ {type(e).__name__}: {e}",

0 commit comments

Comments
 (0)