v0.12.2 — REPL Token Ceiling + TUI Scroll-Mode Fixes
Added
-
Cumulative token ceiling now covers the plain REPL session. Previously
only the gazelle orchestrator's coder-conversation path held a
SessionBudget; the REPL passed none, sorecordTurn/recordCallwere
no-ops and net-of-cache spend was never tracked across messages. One budget
now lives for the life of the REPL process.Turn-count enforcement is deliberately not extended here. The
per-invocationmaxTurnsguard already holds correctly in the REPL, and a
cumulative turn cap is the wrong instrument for interactive use where a
human types every message and watches every response. Measured on a real
96-minute session — 58 turns across 9 messages, 86% cache hit rate, $0.50
total, peak 19 turns in any single message — a 50-turn session cap would
have interrupted that for crossing a count that said nothing about its cost.
Fixed
-
TUI: SS3-encoded arrow keys corrupted the input and flipped the screen.
Terminals in application-cursor mode send\x1bOAfor Up. The input parser
handled CSI (\x1b[A) but not SS3, so the sequence fell through to the
bare-Escape branch: Escape entered scroll mode and the remainingOand
final letter were typed into the input as literal text. Pressing an arrow
key could flip the display to the scroll view and inject garbage.This is very likely the real cause of the "response stalled, then typing
fixed it" reports — the live view was frozen in scroll mode, not the network
connection. Typing a printable character exits scroll mode and redraws,
which is exactly the observed "fix". Normalized the same way
context-tuner.ts'ssplitKeysalready did, including waiting for the
final byte when the sequence is split across reads. -
TUI: typing
qto leave scroll mode silently dropped the character — a
word beginning with "q" lost its first letter.qwas excluded from the
printable-exit path but advertised nowhere.iremains excluded on purpose:
the scroll indicator documents "i/Enter/Esc insert", so it is a deliberate
vim-style command. -
TUI: terminal resizes during an overlay were dropped entirely. While a
command palette, session switcher, context tuner, or confirmation prompt
held the screen,handleResizereturned early and the event was lost,
leaving the scroll region set to the old geometry once the overlay closed.
The resize is now recorded and applied when input resumes.
Known follow-ups (not in this release)
- ESC timeout. A lone Escape stays buffered until the next byte arrives,
so pressing Esc alone does nothing until another key is pressed. Fixing it
needs a ~25–50 ms timer to disambiguate Escape from the start of a sequence;
a fixed timeout can misfire on slow terminals and high-latency SSH, so it is
deferred to its own pass rather than rushed into a patch release. - Archimedes alternator has no budget wired in at all.
alternator.run()
accepts no budget in its options interface, so neither ceiling applies to
that path. Closing it needs a signature change.
Fixed (previously unreleased)
-
npm testsent a real Telegram voice message and overwrote a real API
key. Two tests reached outside their sandbox on any machine with a
configured bot:tests/telegram-voice-live.test.tswas gatedskipIf(isCI || !hasTelegramConfig)
— it skipped on CI and ran everywhere else, firing a live voice note at
telegram.json:default_chat_idon every run. The sends left no trace in the
bot's journal or session history (they came from the vitest process, not the
bot service), which made them look like unexplained "the bot keeps sending me
audio every few hours" behaviour. Now opt-in viaAURA_LIVE_VOICE_TEST=1,
and it requires an explicitAURA_TEST_CHAT_IDrather than falling back to a
real person's chat.tests/provider-wizard.test.tsisolatedXDG_CONFIG_HOMEbut not
os.homedir(), which is whatkey-store.tsuses — sosaveKey()wrote the
fixturesk-test-keyinto the developer's real~/.aura/keys.json,
replacing their DeepSeek credential. It now mocksos.homedir()like the
other filesystem-touching tests. -
Streaming responses could hang forever on cloud providers. An SSE stream
can go silent without the TCP connection closing — no error, no terminating
chunk, the read simply blocks on data that never arrives. Aura waited
indefinitely and showed the user nothing.The SDKs do not cover this, despite appearing to. Both
openaiand
@anthropic-ai/sdkdefault to a 600stimeout, but implement it as
fetch(...).finally(() => clearTimeout(timer))— and the fetch promise
settles when response headers arrive, which for a stream is immediate. The
timer is cancelled before a single chunk of the body is read, so the
documented timeout covers time-to-headers and nothing else.Streams are now guarded by an idle timeout measured between chunks
(src/providers/stream-timeout.ts), default 60s, applied to both the
OpenAI-compatible and Anthropic paths. Total-duration limits would be the
wrong tool: a legitimate turn can run for minutes through tool calls, but a
healthy stream never goes quiet for long once tokens flow. 60s was calibrated
against 529 consecutive-turn intervals from this project's own token log
(median 3.9s, p90 27s — and those measure whole turns including tool
execution, so real inter-chunk gaps are far smaller).On a stall the underlying request is aborted, so the socket is released
rather than leaked. The request is retried once, but only when nothing has
reached the consumer yet — after text has been yielded the agent loop has
already accumulated and displayed it, and re-running would append a second
full response, corrupting both the transcript and the token accounting. In
that case the stall surfaces as a clear provider error instead of hanging.
This mirrors the existing rule inresilient.ts, which retries acquisition
of the first chunk but never a mid-stream failure.Override with
AURA_STREAM_IDLE_MS(values below 5000 are floored;0
disables the guard entirely).