@@ -4,6 +4,103 @@ All notable changes to dirge are documented here. The format follows
44[ Keep a Changelog] ( https://keepachangelog.com/en/1.1.0/ ) and the project
55adheres to [ Semantic Versioning] ( https://semver.org/spec/v2.0.0.html ) .
66
7+ ## [ 0.21.19] - 2026-08-14
8+
9+ ### Fixed
10+ - A panic anywhere in a run hung the TUI permanently. ` panic = "abort" ` is not
11+ set, so a panic unwinds out of the agent task, tokio catches it, and the
12+ event channel closes — and the UI's select arm reads a closed channel as
13+ "nothing more to poll" and silently disables itself. ` is_running ` is cleared
14+ only by events that can no longer arrive, so the run sat at "running" with
15+ nothing left that could change its mind: no error, no timeout, and nothing on
16+ screen to tell it apart from thinking. Tools are awaited inline in that task,
17+ so any tool's ` unwrap ` , a slice index off a char boundary, or a debug-build
18+ overflow went this way. A run's terminal event is now a property of the
19+ task's lifetime rather than of the path it happened to take, so a crash
20+ arrives as the error it always should have been — naming the panic and its
21+ source location — and the prompt comes back. ` --print ` and ACP drain the same
22+ channel and get it too.
23+ - A caught panic reset the terminal of a process that kept running. The panic
24+ hook decided whether to restore the terminal by asking whether the panicking
25+ thread owned it, which is true for every agent-task panic on a
26+ single-threaded runtime — so a survivable panic cleared the screen, left raw
27+ mode, and latched a flag that made the real teardown skip its own reset
28+ later. The hook now only records what happened; whoever survives the panic
29+ reports it, and the terminal teardown prints an unclaimed record after
30+ restoring the screen. A panic some ` catch_unwind ` swallowed now gets one line
31+ at exit instead of living only in a log.
32+ - A tool call a model wrote as text was printed to the user as the turn's
33+ answer. The command ran and the reply was its source code. Two further
34+ defects came out of the same gap: the call never reached the assistant
35+ message, so the next request carried a ` tool ` result with no preceding
36+ ` tool_calls ` (a hard 400 on OpenAI and Anthropic), and those calls carried an
37+ empty id, so two in one turn were indistinguishable to result matching, the
38+ storm signature and the publish guard. Such a region is now hidden exactly
39+ when it dispatches — so a call that names a tool this run does not have stays
40+ on screen, and a ```` ```json ```` block a model is deliberately showing you
41+ never vanishes.
42+ - Renewing an expired provider token froze the whole program. Three transports
43+ resolved their bearer synchronously on the per-request path, and the
44+ refresher spawns an OS thread and joins it around a blocking HTTP exchange.
45+ On a single-threaded runtime that is the only thread: nothing painted, no
46+ keystroke was read, and no timer could fire — including any timeout meant to
47+ bound whatever was stuck. Kimi access tokens live 15 minutes, so it recurred
48+ through a long session. Resolution is off-thread now, with no thread hop at
49+ all on the common path where nothing needs renewing.
50+ - Ctrl+C did not escape the ` question ` modal. It cancels in every other modal;
51+ here it did nothing in the option list, and in the custom-answer field it
52+ typed a literal ` c ` into the answer.
53+ - Nearest-name suggestions for an unknown tool pointed at unrelated tools —
54+ ` exec ` → ` spec ` , ` shell ` → ` skill ` , ` ask ` → ` task ` — in a message with no
55+ hedge, steering a model that wanted a shell toward a spec-management tool.
56+ Of eleven plausible guesses, six were wrong; now one. Names that differ from
57+ a real tool only in case or separators resolve directly.
58+ - Automatic compaction had none of the prompt-injection fencing ` /compact ` has
59+ had all along, and it is the path that runs unattended. It also could not see
60+ tool CALLS — only their results — so a fold recorded outcomes with no record
61+ of what produced them; measured at 0/6 facts preserved that lived only in
62+ call arguments, now 6/6. Both paths are one implementation.
63+ - A ` /compact ` summary entered the loop without the marker an automatic fold
64+ writes, so the next fold could not find it and stacked a second summary
65+ behind it.
66+ - A headless failure printed its error to stderr twice, verbatim.
67+ - A model-supplied ` bash ` timeout ignored the configured ceiling: a model
68+ asking for 3600 got it however low ` timeouts.bash_secs ` was set. It is
69+ clamped by ` timeouts.bash_max_secs ` (default 3600), and the schema the model
70+ reads is rendered from the resolved config.
71+
72+ ### Added
73+ - ` timeouts.tool_call_secs ` (default 600) — a ceiling on one tool dispatch.
74+ Every tool keeps its own tighter bound; this exists so a tool that forgets to
75+ bound itself, or one on a path that skips its own client, cannot stall a run
76+ silently. Tools whose own bound is legitimately larger declare it, and the
77+ budget does not run while a call is waiting on you: the permission prompt,
78+ the ` question ` tool and ` /plan ` approval all wait for a person from inside
79+ the window being bounded, and killing a call somebody is halfway through
80+ approving is worse than the stall this catches. It bounds waits, not blocked
81+ threads — a tool that blocks the runtime never lets the timer be polled.
82+ - A per-turn ` <turn_envelope> ` carrying the volatile session facts (cwd, mode,
83+ model, todos, modified files), rebuilt each turn and replacing the previous
84+ one instead of stacking behind it. The frozen prefix keeps only what does not
85+ change, so it stays cacheable.
86+ - The prompt's tool section is rendered from the live registry rather than a
87+ literal, so prompt-level ` deny_tools ` , umbrella deny rules and dynamic tool
88+ search can no longer make the prompt describe a capability boundary the loop
89+ does not have. Both of these default on: 2 of 6 control runs blew up against
90+ 0 of 6 under treatment.
91+ - Tool failures are classified (misuse / missing-info / transient / fatal).
92+ Transient failures retry with backoff on reads only, and a missing-info error
93+ weighs more when estimating whether a model is coping.
94+ - ` prompt_leak_detect ` , off by default, for a model reciting its own system
95+ prompt back. Advisory mode exists so it can be measured before it is trusted.
96+
97+ ### Changed
98+ - An interrupted mutating tool call no longer tells the model to retry it. What
99+ a call landed — committed, unknown, or nothing — is classified and handed to
100+ the next turn instead.
101+ - A truncated assistant turn is marked incomplete where the model can see it,
102+ not only in the UI.
103+
7104## [ 0.21.18] - 2026-08-12
8105
9106### Security
0 commit comments