Summary
Agent text is rendered into a Rich Panel without escaping, so any prompt or plan document containing a bracketed token that parses as a Rich closing tag — [/nestedType], [/path], [/bold] — raises MarkupError and kills the run.
This is not a cosmetic logging failure. It killed a long-running workflow mid-execution and then killed both resume attempts at the identical byte offset, because the offending text is checkpointed and replayed verbatim. Without hand-editing the checkpoint the run was permanently unresumable.
rich.errors.MarkupError: closing tag '[/{nestedType}...]' at position 145705 doesn't match any open tag
The trigger was ordinary technical prose in a plan document describing an Azure RBAC permission format:
{provider}/{type}[/{nestedType}...]/read
Nothing about that is malformed. It is what the permission format genuinely looks like.
It is worse than "verbose-only"
There are two unescaped sinks, and the second is not gated on verbosity:
# src/conductor/cli/run.py:373-383
should_console = is_verbose() and is_full()
should_file = _file_console is not None
if not should_console and not should_file:
return
if should_console:
_verbose_console.print(Panel(content, title=f"[cyan]{title}[/cyan]", border_style="dim"))
# File always gets full untruncated content
if _file_console is not None:
_file_console.print(Panel(content, title=title, border_style="dim")) # <-- line 383
_file_console is created by init_file_logging, which is called from exactly one condition in each of run and resume:
# src/conductor/cli/run.py:1641 and :2163
if log_file is not None:
init_file_logging(log_file)
No verbosity check anywhere on that path. --log-file alone is sufficient to arm the crash, including under --silent. Since --log-file auto is the natural way to keep a record of a long background run — and is what the Octane workflow skills recommend — this is reachable from the configuration a user is most likely to choose for exactly the long, expensive runs where losing the work hurts most.
Path to the sink
The content is the rendered prompt, which embeds user-supplied plan/workflow text:
executor/agent.py:308 _verbose_log_section(f"Prompt for '{agent.name}'", rendered_prompt)
executor/agent.py:37 verbose_log_section(title, content)
cli/run.py:359 verbose_log_section(...)
cli/run.py:383 _file_console.print(Panel(content, ...)) # unescaped
So the crash is driven by user data, not by anything conductor controls. Any plan doc, workflow file, or agent response containing [/...] is a live grenade — and a reviewer agent quoting a snippet back is enough to detonate it on a run whose plan was clean.
Reproduction
Standalone, no conductor run required. Case 1 is the exact construction from run.py:383:
import io
from rich.console import Console
from rich.panel import Panel
CONTENT = "Permission format: {provider}/{type}[/{nestedType}...]/read"
buf = io.StringIO()
c = Console(file=buf, no_color=True, highlight=False, width=200)
c.print(Panel(CONTENT, title="Prompt for 'coder'", border_style="dim"))
MarkupError: closing tag '[/{nestedType}...]' at position 36 doesn't match any open tag
Same error class and message shape as production. Minimal triggers, all confirmed raising:
| input |
result |
[/nestedType] |
MarkupError: ... doesn't match any open tag |
[/] |
MarkupError: ... has nothing to close |
[/bold] |
MarkupError: ... doesn't match any open tag |
text [/foo] more |
MarkupError at position 5 |
A bare opening bracket is harmless; it is specifically the closing-tag form that raises.
Fix
Two one-line changes, both verified against the repro above to raise nothing and preserve the literal text byte-for-byte:
1. markup=False on the file console (run.py:127). The log file is no_color=True plain text — it has no use for markup interpretation, so this loses nothing:
_file_console = Console(file=_file_handle, no_color=True, highlight=False, width=200, markup=False)
2. rich.markup.escape() on agent content at the call site (run.py:379), for the console path, where markup is genuinely wanted for conductor's own styling but must not be honoured inside untrusted agent text. Note title is conductor-controlled and can keep its [cyan]; only content needs escaping. cli/doctor.py:17 already imports escape, so the dependency and idiom are established in the codebase.
I would suggest both rather than either — they defend different sinks, and (1) is the one that closes the non-verbose exposure.
Worth a wider audit too: this is the instance that bit me, but any Panel(...)/print(...) fed agent- or user-derived text has the same shape.
Workaround, for anyone hitting this mid-run
The offending text is in the checkpoint, so a plain resume re-crashes at the same offset. Rewrite the bracket form in the checkpoint JSON (e.g. [/x] → (/x)) and then resume. That got my run moving again.
Environment
Conductor v0.1.26 (main @ 4a4bd57)
rich 14.3.1
Python 3.14.5 (uv-managed)
OS Windows 11 (10.0.26200)
Not Windows-specific as far as I can tell — nothing in the parse path is platform-dependent — but I have only reproduced it on Windows.
Related
Same run, different failure: #342 / #381 (UnicodeEncodeError in JSON output). Worth stating explicitly that these are not the same bug and should not be fixed together: #342 is an encoding failure at the byte-write boundary, this is a parse failure at the markup boundary. Neither fix prevents the other. I mention it only because both surfaced from the same long run and it would be easy to conflate them.
Summary
Agent text is rendered into a Rich
Panelwithout escaping, so any prompt or plan document containing a bracketed token that parses as a Rich closing tag —[/nestedType],[/path],[/bold]— raisesMarkupErrorand kills the run.This is not a cosmetic logging failure. It killed a long-running workflow mid-execution and then killed both resume attempts at the identical byte offset, because the offending text is checkpointed and replayed verbatim. Without hand-editing the checkpoint the run was permanently unresumable.
The trigger was ordinary technical prose in a plan document describing an Azure RBAC permission format:
Nothing about that is malformed. It is what the permission format genuinely looks like.
It is worse than "verbose-only"
There are two unescaped sinks, and the second is not gated on verbosity:
_file_consoleis created byinit_file_logging, which is called from exactly one condition in each ofrunandresume:No verbosity check anywhere on that path.
--log-filealone is sufficient to arm the crash, including under--silent. Since--log-file autois the natural way to keep a record of a long background run — and is what the Octane workflow skills recommend — this is reachable from the configuration a user is most likely to choose for exactly the long, expensive runs where losing the work hurts most.Path to the sink
The content is the rendered prompt, which embeds user-supplied plan/workflow text:
So the crash is driven by user data, not by anything conductor controls. Any plan doc, workflow file, or agent response containing
[/...]is a live grenade — and a reviewer agent quoting a snippet back is enough to detonate it on a run whose plan was clean.Reproduction
Standalone, no conductor run required. Case 1 is the exact construction from
run.py:383:Same error class and message shape as production. Minimal triggers, all confirmed raising:
[/nestedType]MarkupError: ... doesn't match any open tag[/]MarkupError: ... has nothing to close[/bold]MarkupError: ... doesn't match any open tagtext [/foo] moreMarkupErrorat position 5A bare opening bracket is harmless; it is specifically the closing-tag form that raises.
Fix
Two one-line changes, both verified against the repro above to raise nothing and preserve the literal text byte-for-byte:
1.
markup=Falseon the file console (run.py:127). The log file isno_color=Trueplain text — it has no use for markup interpretation, so this loses nothing:2.
rich.markup.escape()on agent content at the call site (run.py:379), for the console path, where markup is genuinely wanted for conductor's own styling but must not be honoured inside untrusted agent text. Notetitleis conductor-controlled and can keep its[cyan]; onlycontentneeds escaping.cli/doctor.py:17already importsescape, so the dependency and idiom are established in the codebase.I would suggest both rather than either — they defend different sinks, and (1) is the one that closes the non-verbose exposure.
Worth a wider audit too: this is the instance that bit me, but any
Panel(...)/print(...)fed agent- or user-derived text has the same shape.Workaround, for anyone hitting this mid-run
The offending text is in the checkpoint, so a plain
resumere-crashes at the same offset. Rewrite the bracket form in the checkpoint JSON (e.g.[/x]→(/x)) and then resume. That got my run moving again.Environment
Not Windows-specific as far as I can tell — nothing in the parse path is platform-dependent — but I have only reproduced it on Windows.
Related
Same run, different failure: #342 / #381 (
UnicodeEncodeErrorin JSON output). Worth stating explicitly that these are not the same bug and should not be fixed together: #342 is an encoding failure at the byte-write boundary, this is a parse failure at the markup boundary. Neither fix prevents the other. I mention it only because both surfaced from the same long run and it would be easy to conflate them.