Repository navigation
levain 0.6.8
The chat panel says when a model's tool call could not be read, and opens unlocked.
Changed
- A reply that is a model's raw tool-call syntax is no longer shown as the entity's reply. When an open model's tool call fails to parse before it reaches levain, its markup arrives as reply text and that call does not run (glm-5.2:cloud did this on 6 of 10 runs of one file-creation prompt on 0.6.7). The panel and the REPL now show "The model tried to call a tool, but its call couldn't be read, so nothing ran. Ask again, or switch models." When other actions ran earlier in the same turn (for example an action you approved), it says instead that the last call did not run and the listed actions did. The text follows, collapsed under "What the model sent" in the panel (under levain's name, not the entity's), and escaped the way the consent box escapes a held call (printable ASCII as itself, every other character as
\u{XXXX}). The rule: tool-call markup outside a code region is treated as a leak, including inside headings, lists and quotes; replies over 200 kB are not checked. The markup is GLM argument tags (</arg_key><arg_value>) or a<tool_call>wrapper that opens a call (JSON, GLM tags or Qwen3-Coder's<function=...>), and a wrapper and its JSON split across paragraphs count as one. Code regions (fenced and indented code blocks and code spans) are decided by a CommonMark parser, so markup written in code is an answer, as is markup written with an escape or an entity. A reply that is entirely function-call JSON (bare, or the whole of one fenced block) naming only the session's own tools is a leak too. A model has no reason to write tool-call markup in prose, and a reply flagged when it was quoting the markup on purpose still shows the reply's text beneath the notice. The call is never repaired or run.levain run --taskprints the notice and the text on stderr instead of the reply on stdout; with--quietthe stdout payload is still the text as it arrived. The exit code is unchanged (0). The chat API's turn result carriesunreadable_call: truefor such a reply. levain serve --chatopens the cockpit unlocked on macOS. The browser opens with the chat token in the URL fragment (#chat_token=...), which a browser never sends to a server, so the token reaches no access log and no Referer. The page keeps it in that tab'ssessionStorageand removes it from the address bar at once; a reload stays unlocked, and a server restart asks again. The link is printed on every platform (open the cockpit, unlocked: ...). Where the browser would be launched with the URL on its command line (Linuxxdg-open, a browser binary, or$BROWSERset), or when macOS's osascript launcher fails (an SSH session, for example), levain opens the plain URL instead, because other OS users can read a command line, and they are among the callers the token keeps out: open the printed link, or paste the printed token. The per-launch token itself is unchanged.- New dependency:
markdown-it-py>=4,<5, the CommonMark parser that tells the classifier above which parts of a reply are code. An install with theopenhandsextra (what runs a chat turn) already had it, through that extra's own dependencies. - One click starts a session. With one entity registered, the panel shows a "Start session" button for it; the picker appears only when there are several.
- Known limits: the browser's history keeps the link as first opened, with its fragment, and a duplicated or restored tab keeps the tab's
sessionStorage; either works only while the same server runs. Leaked calls in other formats (MiniMax's<minimax:tool_call>, Llama's<|python_tag|>, Mistral's[TOOL_CALLS], DeepSeek and Kimi tool tokens, a GLM call with no arguments, or a fragment cut off before any argument tag) are still shown as a reply. The leaked text is still captured in the entity's memory as that turn's reply. A leak in a reply over 200 kB is shown as the reply, because such replies are not checked. A<tool_call>written before a code block and JSON written after it are read as one call across the code, so that reply is flagged. Markup inside a link's URL or title, or inside a link reference definition, is not read. The size check encodes the whole reply first: on a 300-million-character reply that took 0.03 s and about 286 MiB of transient memory (measured 2026-10-05).