All notable changes to agentty. Versions follow SemVer.
- Six more built-in providers: native Kimi (Sign in with Kimi — OAuth device flow, no API key), plus API-key rows for DeepSeek, xAI (Grok), Mistral, Google Gemini, and Fireworks (
--provider kimi|deepseek|xai|mistral|gemini|fireworks, or pick in^P). The API-key providers each get a dedicatedEndpoint::from_specarm for their host/path (DeepSeek at the root, no/v1; Gemini's OpenAI shim nested under/v1beta/openai; Fireworks under/inference/v1) and read their key from the provider env var (DEEPSEEK_API_KEY,XAI_API_KEY,MISTRAL_API_KEY,GEMINI_API_KEY/GOOGLE_API_KEY,FIREWORKS_API_KEY),-k, or the in-app prompt. Capability inference now recognizes thedeepseek-v4/-reasoner/-chat,grok,gemini, andmagistralfamilies as native tool-callers, and hosted providers ship a small bundled model seed so the picker shows models before a key is set. (registry.hpp,openai/transport.cpp,catalog.hpp,selection.cpp;openai_transport_test.) - Provider picker (
^P) now has a live search filter — start typing to fuzzy-narrow the list (kimi,grok,deepseek…), matching on id, label, and blurb; Backspace trims. Mirrors the model picker (^/). The picker's rows (built-in providers + ACP agents + saved custom hosts + "Custom host…") are now one ordered, single-source row model shared verbatim by the reducer and the view, so the cursor and selection can never disagree — no more parallel index arithmetic. - Device-flow login modals copy BOTH the code and the URL. In the Copilot / Kimi sign-in modal, [[c]] copies the one-time code and [[u]] copies the verification URL (both via OSC 52, so they work over SSH); [[o]] re-opens the browser. The device-flow login path (state, worker, panel, key handling, completion) is now provider-generic — one
DeviceWaitingstate, onedevice_login_asyncworker, onepanel_device_waiting— instead of duplicated Copilot/Kimi copies.agentty loginnow lists GitHub Copilot and Kimi alongside Claude / ChatGPT.
- SSE anti-buffering headers are now single-source. The three directives that stop gateways from buffering/compressing a stream (
cache-control: no-cache, no-transform,pragma: no-cache,accept-encoding: identity) were hand-written in all three streaming transports. Hoisted tohttp::sse_no_buffer_headers()/append_sse_no_buffer(); Anthropic, OpenAI-Chat, and Codex/Responses all route through it so the set can't drift. (io/http.hpp; pinned byopenai_transport_test.) - Reasoning is now uniform across every wire (SSOT). Reasoning effort and streamed chain-of-thought were only wired on the Anthropic (thinking) and OpenAI-Responses (reasoning) transports; the OpenAI-Chat wire — which every hosted API-key provider uses — silently dropped both. Now:
effortis copied once inlower_shared(no per-transport hand-copy); the OpenAI-Chat body encodes top-levelreasoning_effort(o-series, DeepSeek, Grok, Groq, Magistral, Gemini-compat), gated by the same upstreameffort_wire_for; and the Chat SSE parser readsreasoning_content/reasoningdeltas into the sharedStreamThinkingDeltaevent — so DeepSeek/Grok/Gemini thinking now streams and renders exactly like Claude's, with no Anthropic-wire detour. The duplicatedbuild_tools(byte-identical in the OpenAI + Ollama transports) is hoisted towire::openai_chat_tools, and the null-schema guard is shared with the Responses encoder. (provider.hpp,msg_shared.hpp,openai/transport.cpp,ollama/transport.cpp,chatgpt/responses.cpp.)
- Kimi sign-in works end to end. A cluster of fixes to the native Kimi device-flow OAuth: the device-code grant now uses the RFC 8628 URN (
urn:ietf:params:oauth:grant-type:device_code) Kimi requires (was rejected asunsupported_grant_type); the verification URL is rewritten off the deprecated, dead-endwww.kimi.comhost ontowww.kimi.ai; the modal shows and opens the code-embedded URL so it works over SSH/mosh where the browser can't auto-open; and Kimi'sX-Msh-*device-identity headers are sent on the OAuth and the API (chat + models) requests, matching the officialkimi_code_cli— without them the model catalog came back empty. When a Kimi Code account is out of balance the API returns an opaqueHTTP 500; agentty now probes/usagesand surfaces a clear "Kimi credits exhausted" message instead. (src/provider/kimi/*.)
- Tool-heavy turns finish faster — parallel batches + speculative reads. Independent tool calls in one turn now run concurrently (only genuine conflicts serialize; the effect- and path-aware scheduler makes a wide batch always safe), and a pure read-only tool starts the instant its arguments finish streaming — while the model is still writing the rest of the turn — so its I/O overlaps the remaining stream instead of waiting for the turn to end. On a multi-tool turn this hides seconds of file/search time inside the model's own generation. Neither changes results, only when the work happens. The system prompt now nudges the model to fan out independent calls into one message. Optional per-turn telemetry (
AGENTTY_CACHE_PROF=1→/tmp/agentty-cache-prof.log) records prompt-cache hit ratio, per-model TTFT, and tool batch-width. (src/runtime/app/update/stream.cpp,src/runtime/app/cmd_factory.cpp,src/provider/anthropic/prompt.cpp;speculative_dispatch_test.) - The connection re-warms itself after an idle pause. agentty already opens the TCP+TLS+HTTP/2 connection while you type so the first request skips the handshake; now, if a session sits idle long enough for the pooled connection to lapse (~90 s), the next keystroke silently re-dials in the background — so a message after a break is as fast as one mid-flow. (
src/runtime/app/update/composer.cpp.)
- Rewinding to a checkpoint now removes files the agent created after that point. A rewind is meant to restore the worktree bit-for-bit to the checkpointed instant, but files the agent created after the checkpoint were silently left behind — the tree came back "touched files put back," not truly restored. The internal file listings used git's NUL-separated (
-z) output, and the shared subprocess runner scrubs terminal-control bytes from all captured output — including those NUL separators — so both the snapshot and current-file sets parsed to empty and the delete-half of the restore never ran. The listings now use newline separation with raw (unquoted) UTF-8 filenames; a filename literally containing a newline is skipped rather than mis-deleted. Edited files rewind, deleted files return, created files are removed, and git-ignored files (build output, caches) are never touched. New end-to-endcheckpoint_testruns the full create→mutate→restore lifecycle against a real scratch repo. (src/workspace/checkpoint.cpp;checkpoint_test.) - Tool results no longer vanish onto a dead card when a provider reuses tool-call ids. Some OpenAI-compatible gateways mint a deterministic id per (tool, index) — literally
"bash:0"for every bash call on every turn — instead of a uniqueToolCallId. One agent turn holds several assistant messages in the live tail, so the next sub-turn's"bash:0"collided with the previous one's (already Done): the old id lookup matched the first one, so the result was stamped onto the dead card, the real call stayed Pending, and its card hung until the step timeout. Result/progress/timeout/permission routing (with_live_tool) now prefers the first non-terminal call carrying the id, and streaming assembly is already scoped to the current sub-turn's message — so every tool card completes with its own output. (include/agentty/runtime/app/update/internal.hpp;dup_tool_call_id_test.)
-
GitHub Copilot as a first-class native provider (
agentty login→ GitHub Copilot, or--provider copilot). Use your existing Copilot subscription's models — no API key. Sign-in is GitHub's device flow, fully in-TUI: pick GitHub Copilot in the provider picker (^P) and a modal shows the one-time code + opens the browser (ccopies the code via OSC 52 so it works over SSH), polls in the background, and switches on approval — exactly like the Anthropic / ChatGPT OAuth flows. Everything is uniform with the other native providers: the picker row reflects real sign-in state (⚠ sign in/✓ signed in · accounts), andEnteron the active row opens a full multi-account manager (switch / add / remove GitHub accounts). Auth is a short-lived Copilot proxy token exchanged from a persisted (encrypted) GitHub token and auto-refreshed mid-session (skew-safe, single-flight, cross-process-locked), routed to the account's own inference host (endpoints.api— Individual/Business/Enterprise). Model listing is entitlement-aware: it reads the account's real plan + quota (/copilot_internal/user) and shows only the models the subscription can actually use, usable ones starred on top. And it implements Copilot's “Auto” mode — the server-side router that lets Free/Limited plans reach premium models (Claude, GPT-5) that a direct request would reject — as a first-classAuto (best available)entry plus the auto-reachable models, driven by the/models/session+Copilot-Session-Tokenprotocol. (src/provider/copilot/;copilot_token_test.) -
Smart Mode — a self-supervised orchestrator that routes each turn and gets better at your repo (
Ctrl+K → Smart Mode, config overlay onCtrl+S). A role-based execution router built on the orchestrator-workers pattern (Anthropic's multi-agent research + the RouteLLM/cascade literature), with everything toggleable and off = a byte-for-byte no-op. You pin three models to three roles — Strategic (your flagship: the thinking), Implementation (mid: writing code), Utility (cheap: grep/read/summarise) — or leave them to zero-config auto-fill from your live catalog. Nothing ever checks a model name; one pure, tested resolver (agentty::smart) mapsrole → (model, effort). Layers, each an independent toggle in the overlay:- Internal routing — engine-internal utility work (the auto-compaction summary) runs on the cheapest capable model, never the flagship.
- Orchestration — the main turn runs on Strategic and a
<smart-mode>directive teaches it to keep the decisions and DELEGATE mechanical work to subagents (task explorer/coder), with a complete brief, in parallel, wide-then-narrow. - Subagent routing — each subagent's model resolves by its role (explorer→Utility, reviewer→Strategic, coder/tester→Implementation).
- Complexity-scaled effort — a local classifier rates each turn Trivial/Simple/Standard/Complex and scales the Strategic model's reasoning effort accordingly (conservative: ambiguity biases up).
- Cascade feedback — the effort heuristic self-corrects within a session from what the orchestrator actually did (heavy delegation ⇒ it was harder than rated).
- Learned routing (learns across sessions) — a per-workspace prior (
.agentty/routing_memory.tsv, Beta-smoothed) remembers whether each class of turn was under/over-rated in this repo, so the router improves the more you use it — something a stateless router structurally can't do. - Outcome feedback — the ground-truth signal: a failed build/test in the turn, or a user correction on the next turn ("no", "that's wrong", "revert"), is a routing regret that re-rates that class of turn.
- Speculative (opt-in) — on Complex turns, a detached local retrieval warm-up runs while the lead thinks, so the workspace grounding is hot by the time it delegates.
- Plan recall — successful decompositions are captured per-workspace (
.agentty/decompositions.jsonl) and the closest past one is injected into the delegation prompt as a concrete few-shot, so the orchestrator reuses what worked instead of re-deriving it.
Every orchestrated turn surfaces its routing DECISION as a first-class 🧠 Smart Mode card in the transcript (routed model · scaled effort · complexity · active layers); the delegations themselves render as ordinary
tasktool cards. Persisted tosettings.json; single-provider; single-model/Opus-only accounts degrade to no-op per layer. Design:docs/design/smart-mode.md. Tested:smart_mode_test,complexity_test,routing_memory_test,decomposition_memory_test. -
A single "RAG" picker for proactive retrieval (
Ctrl+K → RAG). One decision instead of a wall of toggles: On (inject retrieved context before every turn) / First turn only (ground the first turn, then stay quiet) / Off. Persisted; the advanced retrieval knobs remain env-tunable but out of the UI. Proactive<retrieved-context>blocks no longer leak into the composer's ↑/↓ history recall. -
Fork a thread (
Ctrl+K → Fork thread) — escape a full context window for near-zero tokens. Branches the current conversation into a fresh thread (recordsforked_fromprovenance) that carries almost no context: the parent's full transcript is exported to disk and the fork holds only a small pointer the model reads on demand — so forking is O(1) in tokens no matter how big the parent got, with nothing lost (the transcript is verbatim, not a lossy summary). The new thread opens with a ⑃ Forked card and you pick its proactive-RAG behaviour (per turn / first turn only / off). The original thread is saved untouched. The exported transcript is bounded (512 KB, recency-biased, per-message clip) so even a maxed-out parent forks cheaply. Design:docs/FORK.md. Tested:fork_test,transcript_bound_test,compaction_wire_test.
- A running tool card no longer stays invisible while the reply is still revealing. A tool card that had already flipped to Running could show as running in the status line but have no card in the transcript. The turn view defers a freshly-arrived tool panel off-screen for a beat so it doesn't pop in mid-reveal-glide — but the defer flag was cleared by a per-frame state machine, so if the stream went quiet and no follow-up frame fired, the flag stuck and the running card stayed hidden until some unrelated event (a tick, a keystroke) nudged a repaint. The panel is now never deferred once any of its tools is actually executing (Running/Done/Failed) — deferral only exists to smooth the brief prose-glide-vs-fresh-Pending-card window, so an executing tool's card shows immediately regardless of the defer machine's state. Fixed at both panel-append sites (the single-message body path and the run-merge path). (
src/runtime/view/thread/turn/turn.cpp; tool-boundary / frozen / seam / midrun guards all green.) - Multiple open instances no longer thrash into an OAuth refresh loop when the token expires. With several agentty windows open, an expired OAuth token sent them all refreshing at once. Refresh was guarded only by a process-local mutex, so each instance fired its own refresh POST — and because Anthropic (and ChatGPT/Codex) rotate refresh tokens, the first refresh invalidated the shared token, the losers refreshed against a now-revoked one, and every instance kept seeing a token it didn't mint and refreshed again: the loop. A cross-process advisory file lock (
auth::CrossProcessFileLock— POSIXflock/ WindowsLockFileExon<creds>.lock) now serializes the refresh across processes, and a double-checked re-read after the lock lets the losers adopt the winner's freshly-saved token instead of refreshing again. Best-effort: if the lock can't be taken the old behaviour stands (no worse). Wired through every concurrent refresh path — the per-request refresh (subagent workers), startup refresh, and 401 recovery, plus the ChatGPT/Codex path. (src/io/auth.cpp,src/provider/chatgpt/codex_oauth.cpp,src/runtime/app/cmd_factory.cpp; new fork-basedcross_process_lock_testproves a second process blocks on the lock until the first releases.) - The streaming markdown reveal no longer pops a whole paragraph into view in one frame. A long, soft-wrapped paragraph would materialise smoothly and then, the instant its closing newline arrived, dump its entire remaining body at once (measured as a +172-cell one-frame jump at a paragraph→blockquote seam). The reveal clip rounded up to the end of the completed source line — but prose is one source line per paragraph that wraps at render time, so “the completed line” was the whole paragraph. The tail is now gated exactly at the reveal cursor (with a block-boundary cap so a finished block stays the reveal's last leaf), which is scrollback-safe because the uncommitted tail is redrawn in place every frame. Measured on the deterministic replay harness: worst streaming frame dropped from 172 to 22 cells (tour fixture) and 27 to 6 (smoke), both under the 24-cell CI gate; smooth- and bursty-feed reveal probes pass. A running tool's event-header elapsed is also now frozen (the live seconds live only in the seam-safe footer) so a growing tool panel can't rewrite a committed row's timer. (maya
streaming/build.cpp;src/runtime/view/thread/turn/agent_timeline/agent_timeline.cpp.) - Tools now resolve relative paths against the active project, not the access boundary — fixing widened-
--workspacelaunches. agentty keeps two distinct roots: the access boundary (workspace_root(), the security gate that file tools refuse to cross, widenable all the way to--workspace /) and the active project (project_root(), the launch cwd clamped inside the boundary). A class of tools conflated them, resolving “the project” against the boundary — so under--workspace /a model'sread src/foo.cppresolved to the nonexistent/src/foo.cpp,repo_maptried to map the entire disk,diagnostics/testrancmake --build /build/--manifest-path /Cargo.toml, and the @-file picker + symbol index scanned all of/. All of these — plusgrep/glob/list_dirdefaults,find_definition, git tools, checkpoints, and project-scopedremember— now route through the new centralizedutil::project_root(), so relative paths and project-scoped defaults land in the launched project regardless of how wide the boundary is. Every path is still containment-checked against the boundary afterward, so widening it can never let a relative path escape. By default the two roots are the same directory, so ordinary launches are unchanged. (mcp-cppfs_helpers/git/diagnostics/repomap;src/tool/util/fs_helpers.cpp,src/workspace/{files,symbols,checkpoint}.cpp,src/tool/memory_store.cpp; 18/18 mcp-cpp + agentty tool tests green.) grepandfind_definitionno longer crawl — or return hits from — build/vendor/_depstrees. The ripgrep-backed grep passed no directory excludes, so on a cold cache it stat + gitignore-checked every generated file in out-of-source build trees (tens of thousands in a repo withbuild-*/_depsdirs), and in a repo with no.gitignoreit returned matches straight out of build artifacts. Both backends now prune the same skip-list (build*,cmake-build*,node_modules,_deps,vendor, …) the built-in walker already used, passed to ripgrep as-g '!…'excludes. Measured: on a tree with non-gitignored build dirs a symbol grep dropped from 4 hits (3 generated) to the 1 real source hit, and the walk of the generated trees is skipped entirely. (mcp-cppsearch.cpp/fs_helpers.cpp.)- Thread list is ready instantly at startup (was ~1 s on a large history). The thread picker only needs each thread's title + timestamps, but those keys sit after the multi-MB
compactions/messagesarrays in every thread file, so the metadata-only load still had to stream every byte of every file to reach them — ~1 s for a 247-thread / 281 MB history, during which opening the picker (Ctrl+J) or cycling threads stalled. A smallthreads/index.jsonsidecar now caches that metadata (id → title/created/updated + file mtime); startup reads that one ~30 KB file and only re-parses threads whose on-disk mtime changed, cutting the warm load from ~1000 ms to <1 ms (~1400×). The index is refreshed on every save/delete and self-heals if missing or corrupt (delete it to force a cold rebuild). (src/io/persistence.cpp.) - Kill-to-end-of-line in the composer works again, now on
Alt+K. The readline-standardCtrl+Kkill-to-end binding was dead:Ctrl+Kis claimed app-wide for the command palette before the composer sees the key, so the composer's kill-to-end arm was unreachable. Rebound toAlt+K, which pairs withCtrl+U(kill-to-start) the same wayAlt+D(delete word forward) pairs withCtrl+W(delete word back). (src/runtime/app/subscribe.cpp; new reducer tests incomposer_edit_test, 20/20 green. Full composer keymap now documented in the README and the keybindings page.)
- Composer editing hardened — a queued-message data-loss bug, per-keystroke undo, and three input papercuts. A sweep of the message composer across its reducer, view, and widget layers fixed six issues. (1) Editing a peeked queued message could silently delete the wrong one. After
Alt+↑loaded a queued message for editing, the peek index survived an ordinary keystroke, so pressingEntersometimes removed a different queue slot than the one on screen; a stray edit now cleanly drops the peek and becomes the live draft (symmetric with how history-walk already behaved). (2) Undo now rewinds word-by-word, not character-by-character.Ctrl+Zused to undo a single character and a long sentence blew the whole 64-deep undo history; consecutive typing now coalesces into one undo unit, broken on whitespace and on any non-typing op (paste, delete, cursor move, undo/redo, history/queue walk), so oneCtrl+Zafter a paste actually reaches the pre-paste state. (3) The live token / word / line counters were wrong whenever an attachment existed — they counted the short chip caption ([Pasted text · 412 lines · 14 KB]) instead of the payload, so a 400-line paste read as “1 line, ~10 tok”; the counter now measures the expanded attachment bodies, i.e. what actually goes to the model. (4)Ctrl+←/→word motion now steps over a run of punctuation as one unit ())))was four presses). (5) The/command palette opens on any line-leading slash, not only on a completely empty composer, matching shell muscle memory and the existing@/#word-boundary rule. (src/runtime/app/update/composer.cpp,src/runtime/view/composer.cpp, mayawidget/composer.hpp; six new reducer tests incomposer_edit_test, 17/17 green, no flicker regression.)
- ACP
bashcards now always show their output, andedit/writecards render identically. Two card-rendering defects when agentty runs as an ACP agent: (1) abashrun through the live-terminal fast path attached only an ACPterminalcontent block — and because agentty releases the terminal the instant the command finishes, Zed dropped the released widget and the completed card showed nothing. The completion now also carries the captured stdout/stderr as a fenced text block ((no output)for a silent command), so the output is always visible and survives session reload; the internal (sandboxed / no-terminal-capability) path fences command output as a monospace log instead of markdown-escaping it. (2)writeannounced its diff with a nulloldText(a special "new file" affordance) whileeditannounced a normal before/after, so the two looked different in Zed;writenow announces an empty-stringoldText, giving both the same diff-card shape, and the authoritative on-disk diff still replaces it on completion. (src/acp/server.cpp:run_bash_via_terminal()completion,result_content_block()execute-kind fencing,announce_diff(); covered by a newbash-output assertion inacp_integration_test.) - ACP shell parity with Zed's native agent — slash-command menu, model picker, sign-in, and live thread titles. Running agentty as an ACP agent now lights up the same panel affordances Zed gives its own agent, closing the "second-class external agent" gaps: (1) session/new + load emit an
available_commands_updateso Zed's composer/menu is populated with/compact,/new, and every installed skill as/<skill-name>; (2) amodelconfig_option_updateadvertises the provider's model catalog, so you can switch models from Zed's per-session dropdown instead of relaunching with-m(the existingsession/set_config_optionalready applied the choice — now it's discoverable); (3) when agentty has no credentials,initializeadvertises anauthMethod, so Zed renders a "Sign in" prompt instead of erroring on the first turn; (4) the first message of a session pushes asession_info_updatewith a derived title, and restored sessions re-announce their title on load, so Zed's thread sidebar always shows a meaningful name. (src/acp/server.cpp:available_commands(),model_config_option(),emit_session_config(); covered by new assertions inacp_integration_test.) - ACP tool cards render like the native agent —
Readresults are line-numbered, tool output is markdown-safe, and every card has a body. When agentty runs as an ACP agent (driven by Zed or any ACP client), a completedreadnow sends its card body as a fenced, tab-numbered excerpt (1\talpha,2\tbeta, …) anchored at the read's offset, matchingclaude-code-acpso Zed renders a clean gutter-numbered file view instead of a raw blob. All non-diff tool result text is now markdown-escaped, so literal*, backticks,#,|, and<in file/grep/command output render verbatim in the card instead of being parsed as bold/headers/tables/HTML. Announce-time card bodies were widened to match claude-code-acp so no card renders as a bare title:bash/test/diagnosticsshow the fenced command,web_fetchshows its prompt,taskshows the sub-agent prompt (now also on replay), and any MCP / unknown tool pretty-prints its raw input as a ```json block. The same shaping is applied on session replay, so a restored thread's cards look identical to when they first ran. (src/acp/server.cpp: `result_content_block()`, `markdown_escape()`, widened `command_content()`, `prompt_content()` wired into `make_tool_call`; covered by assertions in `acp_integration_test`.) - The RAG engine now shows its work — a retrieval “funnel” in every
search_docs/search_coderesult. Retrieval used to be an opaque black box: the model (and you) got a ranked list and a terse(mode: hybrid+ctx, reranked)label, with the per-stage trace hidden behind an env var nobody set. Now every result is headed by a readable funnel that walks the candidate set through each stage it actually passed, with the real counts the engine recorded — e.g.hybrid: 47 candidates ↳ reranked top 30 ↳ dedup 30→24 ↳ stitch: merged 3 adjacent ↳ autocut 24→8 ↳ top-8— plus a one-line headline naming the retriever, the fusion method, and the confidence. You can see why eight passages came back instead of trusting a label. (AGENTTY_RAG_TRACE=0restores the compact one-line header.)
- Retrieval quality upgraded to rag-cpp's measured-best pipeline. agentty's hybrid search now defaults to adaptive convex (TM2C2) fusion instead of plain reciprocal-rank fusion — rag-cpp benchmarks it as beating RRF on NDCG because it preserves the score distribution RRF discards, and the adaptive variant additionally shifts per-query weight toward whichever retriever (lexical vs. dense) is more confident on that query. Two new refinement stages from rag-cpp's
Pipeline::best()are wired in: near-duplicate dedup (folds paraphrase/boilerplate copies so an LLM context window isn't spent re-reading the same passage) and relevance autocut (trims the low-relevance tail at the score knee, so a query with three strong answers returns three, notkpadded with weak matches). All are on by default and individually toggleable (AGENTTY_RAG_FUSION=rrf,AGENTTY_RAG_ADAPTIVE,AGENTTY_RAG_DEDUP,AGENTTY_RAG_AUTOCUT). Also picks up rag-cpp's BlockMax-WAND BM25, robust (winsorized) fusion, AVX-512/VNNI kernels, and a cache-backed rerank stage under the hood. - Retrieval spends fewer tokens for the same answer — five research-backed frugality levers on the
search_docs/search_codeoutput path. Retrieval is the one place agentty spends model-context tokens on your behalf, and the output budget was previously flat and split evenly by passage count — a rank-8 hit at confidence 0.11 got the same byte allowance as the rank-1 hit at 0.88, i.e. equal tokens on signal and noise. The output path now: (1) applies a relevance floor (AGENTTY_RAG_RELEVANCE_FLOOR, default 0.30) that drops the low-confidence tail the model ignores anyway before spending any tokens on it; (2) allocates the budget by score-proportional water-filling (AGENTTY_RAG_BUDGET_GAMMA, default 1.5) so confident passages get room to be complete and marginal ones get a tight excerpt; (3) scales the total budget by CRAG confidence (AGENTTY_RAG_CONF_BUDGET_FLOOR, default 0.45) so a barely-passing retrieval injects a cheap block instead of reserving the full ~3k tokens; (4) compresses oversized prose passages with a model-free, LLMLingua-style extractive pass (AGENTTY_RAG_EXTRACTIVE, on by default) that keeps the highest query-overlap sentences and drops the filler between them — something the old contiguous line-window couldn't (code/config keep the line-window, where contiguity is load-bearing); and (5) caps the unprompted proactive<retrieved-context>block independently and more tightly (AGENTTY_RAG_PROACTIVE_BYTES, ~6 KiB) since it's spent without the user asking. On a realistic 161-file corpus these cut retrieval output ~13% (1,845 → 1,605 est. tokens/query) with identical ranking (recall@10 1.000, MRR 0.968, nDCG@10 0.976) — savings scale with passage size. A newAGENTTY_RAG_MEASURE=1 agentty rag-benchmode runs queries through the real output path and reports bytes/estimated-tokens so you can quantify any lever on your own corpus by toggling it and diffing. Every lever is on-by-default-and-tunable via env, no rebuild. The Retrieval and Configuration docs cover each knob.
- Model picker now sorts by actual strength, not a hardcoded family bucket. The previous sort ordered models by a fixed family position (Opus, Sonnet, Haiku, Fable, Mythos, in that order), so the Fable/Mythos lane — Anthropic's newest flagship-tier models, same strength class as Opus, just a different codename — always sank to the bottom of the picker regardless of how new or capable it actually was. The new
agentty::model_picker_lesscomparator (include/agentty/domain/catalog.hpp) is the single source of truth for picker ordering:ModelCapabilities::tier()descending (Flagship — Opus and Fable/Mythos — always leads, then Mid/Sonnet, then Cheap/Haiku, then Weak), then newest generation/revision within a tier, then a family tie-break for stable grouping among same-generation peers. Any future family name now sorts by what it is, not by where its name happened to land in a hand-written list.
- Smart, tunable auto-compaction + entitlement-safe 1M-context model variants. Auto-compaction used to fire at a fixed absolute margin (
context_max - 17k), which was inconsistent across window sizes (98% on a 1M window, 91% on 200K) and forced a summarization pass long before a big-window model actually needed one. It now fires at a percent of the window (StreamState::compaction_threshold(), default 90%, clamped to always leave 20K tokens of output headroom) that's user-tunable from the command palette's new Compaction depth entry (75% Aggressive → 90% Balanced → 95% Deep, persisted tosettings.json). Separately, the summarization request itself now runs on the cheapest capable model on the active provider instead of the flagship model you're chatting with — compacting used to mean a full ~context-max-sized flagship-priced input on every trigger; it now costs a Haiku-class summary. On the model side, the picker offers a(1M context)variant right below every Sonnet/Opus/Haiku 4+ model when signed in with Claude Pro/Max OAuth — matching Claude Code's own model catalog (verified against its shipped binary: the base window is always 200K, and 1M is an explicit, entitlement-gated picker row, never auto-detected from account tier). Selecting it widens the tracked context window to 1M and requests Anthropic's extended-context beta; the picker marker never reaches the wire. repo_mapis pollution-proof against nested projects. The ranked codebase skeleton could surface sibling-project source (submodules, vendored checkouts, unrelated repos living under or alongside the workspace). The walk now stops at any nested repository boundary (a subdirectory carrying its own.git/.hg/.svn/.jj— including a submodule's gitlink file, not just a directory) and re-asserts every accepted file is genuinely inside the workspace before it becomes a graph node.- Subagents route to the cheapest capable model on the active provider. Read-only
taskroles (explorer,reviewer) no longer spend flagship-model tokens on fan-out exploration — they run on the cheapest model that still passes a capability floor (tool support, dispatchable asset, non-Weak tier);tester/coder/generalkeep the parent model since they mutate the workspace. Model strength is derived provider-relatively from the id (no vendor ships a comparable power number), the router never routes up, and a single-model or Opus-only account sees no change. Subagents also never enable reasoning/thinking beyond the parent's setting.
- Provider transport ingress is fully unified across all four transports (Anthropic, OpenAI-compat, Ollama-native, ChatGPT/Codex Responses) — no more per-transport copies that could silently drift. Retry-After backoff parsing, strict UTF-8 scrubbing, the leaked-tool-call prefix sniffer, all three token-usage wire shapes, and OpenAI-family auth-header emission each now live in exactly one shared helper. Fixed real bugs found while unifying: Ollama's Retry-After parsing was silently ignoring server backoff entirely, and two transports accepted invalid overlong/surrogate UTF-8 that the canonical scrubber now rejects.
- State-of-the-art MCP authorization — interactive OAuth 2.1 + PKCE login for remote servers (spec 2026-07-28). agentty can now sign in to an OAuth-gated MCP server end-to-end:
agentty mcp-login <server>probes the server, walks the RFC 9728 protected-resource → authorization-server metadata chain, registers a client, opens your browser at the PKCE (S256) authorize URL, catches the redirect on an ephemeral loopback callback server, and exchanges the code for an issuer-bound token that's sealed at rest (chmod 600,~/.agentty/mcp_tokens/<server>.json) and auto-refreshed on expiry — the HTTP transport injects a freshBearerper request.mcp-logout <server>clears it;mcp-statuslists every configured server and which are authorized. The full 2026-07-28 hardening is implemented in the dependency-free, unit-testedmcp-cppauth layer: RFC 9207 issuer validation (SEP-2468 — the authorization response'sissis checked against the AS issuer before the code is redeemed, closing the AS mix-up attack),application_type=nativeDCR (SEP-837, so the loopback redirect a CLI needs isn't rejected), issuer-bound credentials (SEP-2352 — a token is stamped with the AS that minted it and never replayed elsewhere), and CIMD (Client ID Metadata Documents — present a stablehttps://URL as theclient_idwith no DCR round-trip). Client registration falls through three paths in 2026-07-28's preferred order: a--client-id <https-url>/ mcp.json"client_id"CIMD URL, a pre-registered publicclient_id(also via$AGENTTY_MCP_CLIENT_ID), else Dynamic Client Registration — so login works even against an authorization server that offers no DCR endpoint. A 401 from a server is parsed (RFC 9728WWW-Authenticatechallenge → resource-metadata URL, with a/.well-known/oauth-protected-resourcefallback) into an actionable error telling the user to runmcp-login. Portable SHA-256 (FIPS 180-4 KAT-verified), base64url, and PKCE are all in-tree; the loopback server is Winsock/BSD-portable and the whole path is Windows-verified. The stateless MCP server helpers were hardened alongside it (CBOR-based binary-safe request-state codec that no longer crashes on non-UTF-8, endianness-stable MAC,require_capabilities(), SEP-2243 header-routing validation) and the protocol surface caught up to 2026-07-28 (tasks/update,subscriptions/listen, a deprecated-method registry).
- Windows: the MSI now installs per-user with no admin / UAC prompt. The installer was
perMachine— it wrote to%ProgramFiles%and the systemPATH, so a plain double-click hit a UAC elevation wall. It's nowperUser: agentty installs to%LocalAppData%\Programs\agentty, edits only yourPATH, and registers a per-user Add/Remove-Programs entry — nothing needs administrator rights (the binary is self-contained; the PATH edit is yours). The winget manifest declaresScope: userto match, sowinget install agenttynever tries to elevate either. - Windows builds again (and the whole release is green on all six targets). The rag-cpp retrieval engine isn't yet MSVC-portable (POSIX headers and GCC-only SIMD attributes); rather than block every Windows package, agentty degrades gracefully on MSVC — CMake skips retrieval and the adapter compiles a no-op fallback, so the Windows binary ships with every non-retrieval feature working. The macOS standalone build no longer dies configuring rag-cpp's Metal backend (forced off — agentty's retrieval is CPU-only).
- Package channels can no longer silently fall behind a release. Every downstream publisher (AUR / Homebrew / scoop / winget) is gated behind a build leg, so one failed/slow leg used to skip its publisher — the tag went public but the package stayed stale (which is exactly how an AUR out-of-date flag and a winget hash-mismatch happened).
reconcile-manifests.ymlnow re-pins AUR/Homebrew/scoop from a release'sSHA256SUMSautomatically after every release run and weekly; the winget submission gates onchecksums-finaland verifies the MSI hash againstSHA256SUMSbefore opening a PR, so it can never submit a hash that drifted from the released asset.
- Homebrew is a clean two-line install (
brew tap 1ay1/tap && brew install agentty); the formula now installs by the release asset's real name and prints a first-run hint.
-
Retrieval got faster without getting weaker — one batched embed round-trip, and two research-backed vector-cost levers. A latency + throughput pass over
search_docsthat changes nothing about default result quality (the fast wins are pure plumbing; the new precision knobs are opt-in and rescore back to full fidelity). (1) One/api/embedround-trip per source, not N. A single search fans into many dense probes — the query, conversation-carryover + multi-hop facets, RAG-Fusion paraphrases, a HyDE passage — and each used to embed in its own blocking round-trip, serially (5–8 on a fully-expanded query). They now batch into ONE call per source (/api/embedalready accepts an array):Corpus::search_fusedpre-embeds every variant together, and — the load-bearing half —KnowledgeRouter::retrieve_multinow asks each source once for all variants (via a newKnowledgeSource::retrieve_multiseam thatCorpusSource/McpResourceSourceoverride) instead of looping single-query retrieves, so the batching actually reaches the funnel. (2) Matryoshka ANN truncation (AGENTTY_RAG_ANN_DIM, off by default) —nomic-embed-text-v1.5and the e5/BGE MRL models pack their signal into the leading dims, so the HNSW graph can be built + walked on a dimension prefix (e.g. 256 of 768): ~2.3× faster graph walk at ⅓ the graph memory, with the full-dimension rerank stages recovering any precision. A changed value auto-rebuilds the cached graph; no cache-format bump. (3) Binary quantization (AGENTTY_RAG_BINARY, off by default) — walks the graph on 1-bit-per-dim sign codes with popcount Hamming, then rescores the returned pool with the exact float cosine (the HuggingFace “binary embedding quantization” pattern): binary recall, float precision (~0.97 recall@5 in-repo), ~2.5× faster (≈ 3.4× stacked with truncation). Sign codes are derived from the stored vectors on load, so the on-disk cache is unchanged and the flag toggles with no rebuild. (4) Relative Score Fusion (AGENTTY_RAG_FUSION=rsf) — an alternative to the default rank-based RRF that min-max-normalizes each list and weighted-sums, preserving the score magnitude RRF discards; A/B it on your corpus. Everything is off-by-default or behaviour-preserving; the Retrieval and Configuration docs cover every new knob, and a pluggableset_embed_backend()seam makes the batching testable offline (the suite proves N variants → exactly one embed call). -
Advanced retrieval: the engine now learns, converses, hops, and measures. Five capabilities that move
search_docsbeyond a static funnel — all pure C++/STL, zero new dependencies, default-on where deterministic, and each degrading to the previous behaviour on any failure (src/rag/advanced.cpp). (1) Learning loop — agentty closes the feedback loop no other terminal agent closes: every surfaced passage counts a "use"; when the agent follows up byreading the file a passage pointed at, that's a "win" (implicit relevance judgment, hooked at the tool-dispatch seam). The Beta-smoothed per-passage win-rate persists to.agentty/rag_feedback.tsvand folds into ranking as a bounded multiplicative nudge (×0.85–×1.15, neutral with no history — a fresh workspace ranks byte-identically) so retrieval gets measurably better the more you use it (AGENTTY_RAG_LEARN=0off). (2) Conversation carryover — a recency-decayed salience pool over recent queries lets a vague follow-up ("how does it handle errors?") gain the entities under discussion as an EXTRA RRF probe; deterministic, never replaces the original query, recall can only rise (AGENTTY_RAG_CARRYOVER=0). (3) Multi-hop decomposition — compositional questions ("how X works and how Y blocks Z") split on clause connectives into per-facet probes riding the existing multi-query fusion, gated conservatively (≥2 clauses × ≥2 content terms) so ordinary queries pass untouched (AGENTTY_RAG_MULTIHOP=0). (4) Late-interaction reranking — ColBERT-style sentence-level MaxSim upgrades the chunk-level embedding rerank tier at the same cost class (one batched/api/embedround-trip): the query aligns to each candidate's BEST sentence (blended with the runner-up for corroboration) instead of the blurred whole-chunk vector (AGENTTY_RAG_LATE=0falls back to chunk cosine). (5) GraphRAG-lite — the author-curated relevance graph hiding in markdown links: top hits' outbound](doc.md)targets are followed one hop and the linked documents' lead chunks join the context as supporting material, scored below every direct hit (AGENTTY_RAG_GRAPH=0). -
agentty rag-bench [dir]— the eval harness that makes every stage provable. Retrieval engineering without measurement is vibes; this makes the funnel's anatomy inspectable on the USER'S corpus, offline, in milliseconds: it synthesizes known-item queries from sampled chunks (most-discriminative terms by tf×idf — deterministic, reproducible, no LLM), then reports recall@k / MRR / nDCG@10 / mean-µs across the retrieval ladder (BM25-only → hybrid+PRF → +feature-rerank → +MMR), so a regression or a win on any corpus is attributable to exactly one stage and every env toggle can be tuned against numbers instead of guesses. -
State-of-the-art local retrieval engine behind
search_docs— now documented, and better by default. A sustained pass took agentty's RAG from "good" to frontier-grade for a fully local, dependency-free, offline-capable engine, and made the default path reflect it (not just the fully-tuned one). New this cycle: (1) Pseudo-relevance feedback (RM3-lite) query expansion, default-on — an initial BM25 pass harvests the most discriminative terms from the top hits (feedback frequency × corpus rarity) and fuses in a second, down-weighted BM25 probe over{query + those terms}, recovering vocabulary-mismatch hits (synonyms, the exact spelling the docs use) with zero model/network cost (deterministic, sub-ms;AGENTTY_RAG_PRF=0disables). (2) Parent-document (small-to-big) retrieval, default-on — each surviving small chunk is stitched back into its adjacent siblings from the same document so a precise hit is read in context, without widening the probe (AGENTTY_RAG_PARENT). (3) HyDE — Hypothetical Document Embeddings (AGENTTY_RAG_HYDE, opt-in) — the LLM hallucinates a short answer-passage whose embedding lands the probe near the real answers. (4) Source-agnostic multi-query fusion — query expansion and HyDE now help every knowledge configuration (docs, skills-only, memory-only, MCP, any mix), not only when a docs folder exists, via a newKnowledgeRouter::retrieve_multithat fans every probe across every source and fuses all ranked lists in one RRF pass. (5) Graded cross-encoder rerank rubric — the opt-in generative reranker now scores against an anchored 0/2/4/6/8/10 relevance scale that judges answering over keyword overlap. Full engine (all local, no dependencies): hybrid BM25 + dense embeddings + RRF, HNSW ANN, contextual-retrieval breadcrumbs, PRF, feature-fusion + embedding-cross-encoder rerank, MMR, extractive compression, parent-document expansion, corrective retry, per-turn cache, and proactive pre-turn injection. A brand-new Retrieval doc page walks the whole funnel, and the Configuration table now covers everyAGENTTY_RAG_*/BM25_*knob. Degrades gracefully at every stage — no embeddings → BM25-only, Ollama unreachable mid-search → the affected stage no-ops and retrieval continues. -
--auth-header NAME— custom auth header for OpenAI-compatible gateways. Custom--provider host[:port]endpoints (and presets) could only authenticate with the hard-codedAuthorization: Bearer <key>, which locked out self-hosted / enterprise gateways that expect the key under a different header name (e.g.X-API-Key). The new session-scoped flag overrides the header name; the key (from-k/ the in-app paste /OPENAI_API_KEY) goes out raw under it, on every OpenAI-family request (chat completions,/v1/modelslisting, the Ollama capability probe), and survives live provider switches (^P). Unset keeps the standard bearer header; the Anthropic path is untouched. (#5)
-
The prebuilt Linux binary itself is now a real standalone binary — v0.2.7's
curl \| shcrash is fixed at the root, not just papered over. v0.2.7'sagentty-linux-x86_64was a musl dynamic-PIE masquerading as static:readelf -dshowedNEEDED libc.musl-x86_64.so.1and it carried noPT_INTERP, so the kernel mapped it at a random base and jumped to an unrelocated entry point → instantSIGSEGV(exit 139) on any glibc host (it only ran on Alpine, where the musl loader happens to sit at the baked path). Root cause: Alpine's default-PIE musl GCC does not pull libc from the static archive under-static-pie— it emits a loader-dependent dynamic-PIE. (Confirmed empirically:-static-piealone,-static-pie -static(link error — non-PIE CRT), and-Wl,-Bstatic --no-dynamic-linkerall fail on Alpine 3.21 / GCC 14.2.) The fully-static Linux build now links with-static -no-pie, which pulls libc from the archive and emits a classicET_EXECwith noNEEDEDand noPT_INTERP— a true standalone binary that runs on every Linux userland (glibc Debian/Ubuntu/Fedora, musl Alpine, 64-bit Raspberry Pi OS). Termux/Android (which needs a PIE) is available via the opt-in-DAGENTTY_STATIC_PIE=ONon a suitable musl toolchain. Backing all of this: a build-time ELF-shape assertion (cmake/assert_static_pie.cmake, aPOST_BUILDstep on every fully-static build — release CI, localAGENTTY_FULLY_STATIC=ON, and the installer's--build/ auto-fallback) hard-fails the compile if the result has aNEEDEDentry or aPT_INTERP, so a downgraded, un-runnable artifact can never be packaged or shipped again regardless of pipeline. Verified: the guard fails (exit 1) on the actual broken v0.2.7 binary and passes on the new-static -no-piebinary. -
Agent tools hardened against wedging, injection, and silent corruption. A robustness pass over the tools the agent leans on hardest — no new features, four concrete correctness/safety fixes. (1)
bashcan no longer run forever. The command timeout was an idle timer (it reset on every line of output), so a steadily-chatty command (yes,tail -f,ping, a progress-spamming loop) never tripped it and ran until the capture cap, then kept spinning. There's now a hard wall-clock ceiling from spawn (defaultmax(timeout×20, 10min)) enforced with SIGTERM→SIGKILL, so a long chatty build is never cut short but nothing runs unbounded. (2)find_definitionno longer touches a shell — it built ash -cstring and interpolated the symbol between single quotes, so a symbol containing'could break out; it now runs ripgrep via a literal argv likegrepalways did. (3)editcan't silently corrupt on a rolled-back batch — when one edit in a multi-edit batch tripped itsexpected_replacementscheck, the rollback re-ran the fuzzy matcher on the prior edits, which could land a different region; it now restores an exact pre-edit snapshot. (4)git_diff/git_logreject arefstarting with-(a smuggled git option like--output=…in the revision slot). -
Checkpoints keep working under
--workspace /(full-power launches). The checkpoint layer resolved the enclosing git repo fromutil::workspace_root()— but that root is the filesystem access boundary, which power users routinely widen with-w /for unrestricted disk access. Probinggit -C / rev-parsethere fails (/is never a repo), so every checkpoint silently died: no snapshot on submit, no divider, and "Rewind to checkpoint" toasted "checkpoints need a git repo" even inside a real project. The repo is now discovered from the process cwd — the directory agentty was launched from (i.e. the project), which it never chdir's away from — sogit rev-parsewalks up to the true enclosing repo no matter how wide the sandbox gate is.-w /now widens access without destroying the project identity that checkpoints, git tools, and diffs key off. Falls back to the workspace root only if cwd is unreadable.
- Compaction and checkpoint markers now read as real turns, not floating chrome. Two seams looked broken. (1) The
Conversation compactedsummary was rendered as a bare full-width rule with no speaker identity — a stray divider hanging in the transcript. It's now a genuine minimal system turn: a≡glyph, a muted rail, aCompactedheader + timestamp, and a one-line body (“Earlier conversation summarized to reclaim context.”). The raw summary prose is still elided from the view (it's written for the model, can be many KB) but the model receives it on the wire unchanged. (2) A checkpointed user turn drew a separate full-width─── [↺ Restore checkpoint] ───CheckpointDividerabove the rail, which read as a hard interruption. The marker now lives inline in the turn's own meta line as a subtle· ↺ checkpointtag — part of the turn, not a banner over it. Both changes flow through the sharedturn_config, so the frozen and live builders stay byte-identical at the freeze seam (seam symmetry test green, 442/442). The inter-run wire boundary divider (a true between-runs marker) is unchanged. - Picker rows never overflow, at any terminal width. The rewind-checkpoint picker feeds free-form prompt text as the row's primary label and an async
N files +A −Bdiffstat as the secondary — two potentially-long cells on one row. maya'sPickerrow now lays leading and trailing out with real flex-shrink weights (leading grows to fill and is first to truncate; trailing holds its natural width and only shrinks reluctantly), both ellipsis-clipped, so a paragraph-length preview meeting a fat diffstat degrades gracefully instead of spilling past the border. The preview is also pre-clamped to a readable length (UTF-8-safe) and the picker'smin_widthwas brought in line with the rest of the family.
- New
| shrink(factor)DSL pipe. The flex engine already supportedflex_shrinkend-to-end (FlexStyle, the Yoga solver,BoxBuilder::shrink) but there was no way to set it from the declarative DSL — only| grow. Addedshrink()as the exact mirror ofgrow()(runtime pipe tag + factory +WrappedNodeplumbing in both build branches), so responsive rows can now saytext(a) | grow(1) | shrink(3)to control which cell yields space first when a row gets tight.
- agentty now identifies honestly to Anthropic instead of impersonating Claude Code. The OAuth/subscription transport used to spoof the official Claude Code CLI byte-for-byte to get subscription tokens accepted — a fake
user-agent: claude-code/2.1.113,x-app: cli, the full Anthropic JS SDKx-stainless-*platform fingerprint, thex-stainless-helper: BetaToolRunnertag, and a Claude-Code-shapedmetadata.user_id— which is exactly the masquerade pattern that gets subscription accounts flagged under Anthropic's ToS. The transport now announces itself:user-agent: agentty/<version>,x-app: agentty, nox-stainless-*SDK fingerprint, noBetaToolRunnertag, and a plain{device_id, session_id}agentty client id (no fakeaccount_uuid). It keeps only what the API functionally requires —anthropic-version, theanthropic-betafeature flags (includingoauth-2025-04-20for subscription tokens), and the auth header. If Anthropic's edge ever hard-requires a first-party client signature, that's a ToS boundary agentty surfaces to the user ("use an API key or another provider"), not one it quietly circumvents by masquerading. - Release binaries are now smoke-tested on a foreign libc before they ship. v0.2.7's Linux prebuilt was a musl static-PIE binary that segfaulted immediately on glibc/Debian, and nothing in the release pipeline caught it: the workflow only ran
readelfELF-shape checks (and only on aarch64, as non-fatalWARNs) and never executed the binary. A binary can pass everyreadelfcheck and still crash at startup on a foreign libc. Each Linux build leg (x86_64 / aarch64 / i686) now (1) hard-fails — not warns — if the static-PIE link degraded (aPT_INTERPorNEEDEDentry, or a non-ET_DYNtype), and (2) runs a--versionsmoke test inside a clean Debian (glibc) and Alpine (musl) container (matching i386 images for the 32-bit leg), failing the release if the just-built binary won't launch on either. The aarch64 smoke test runs on real ARM silicon (no QEMU); i686 reuses the QEMU already set up for its build. The x86_64 release-marchbaseline was also pinned tosse2(the universal amd64 ISA) for intent-clarity — the GCC/Clang path never emitted the oldavx2value anyway, so codegen is unchanged and the binary keeps running on pre-Haswell CPUs. install.sh --buildsource-build path + automatic fallback for broken prebuilts. When a release binary can't run on the target system — e.g. v0.2.7's musl static binary with noPT_INTERPsegfaulting immediately on Debian/glibc — the one-liner installer used to be a dead end: it downloaded the broken artifact,chmod +x'd it, moved it into place, and left the user stuck. Now the installer (1) accepts a--buildflag that skips the prebuilt download and compiles from source (git clone --recursiveat the requested ref →cmake -DAGENTTY_STANDALONE=ON→ build → install to$PREFIX/bin), with clear preflight errors when git/cmake/a C++26 compiler is missing; and (2) auto-falls-back to that same source build when a downloaded binary fails to print its version (segfault / exec failure / ABI mismatch), instead of installing something that doesn't run. README +--helpdocument both.--buildis a no-op-safe addition — the default fast path (download prebuilt) is unchanged.- Rewind to any checkpoint, with a diff preview. Every user turn inside a git repo already pins a worktree snapshot and draws a checkpoint divider above it; now all of those points are reachable, not just the newest. "Rewind to checkpoint" in the command palette opens a picker listing every checkpointed turn (turn number + prompt preview + relative time), and each row shows a
N files · +A −Dsummary of what the worktree has changed since that point — computed asynchronously (tree-vs-treegit diff --numstatagainst a scratch index) so opening is instant even on a big repo and a rewind is never blind.↑↓/j/kmove,Enterrewinds (the existing destructive files-and-transcript revert, with the old prompt refilled in the composer),Esccancels. Gated on an idle session and a real git repo, with a friendly toast otherwise.
- First-run onboarding starters. On a genuine first run — thread history has loaded and there are no saved conversations yet — the welcome screen now shows a compact "New here? Try one of these" card with three concrete example prompts (understand the codebase / find-and-fix a bug / add a feature and run tests). It teaches the three things agentty is for so a brand-new user isn't staring at a blank composer wondering what to type. A returning user with any history never sees it, so the welcome stays clean. Gated on
!threads_loading && threads.empty(), so it can't flash during the async thread-load at startup.
- README refreshed. Full install matrix (apt/dnf/zypper/AUR/apk/brew/scoop/winget + curl one-liner + from-source), a first-run note in Getting Started, the Windows-native Ctrl+G shell dispatch, and a maintainer "Releasing" section documenting the one-command
cut-releaseflow.
- The
agentty-linux-aarch64binary now runs on Termux/Android and 64-bit Raspberry Pi. The standalone aarch64 binary is built-static-pie(passed to both compile and link steps, working around the Alpine/musl GCC spec bug where link-only-static-piepicksScrt1.ooverrcrt1.oand drops the program header). The result is a fully staticET_DYNwith noPT_INTERPand a validPT_PHDR— so it loads on Android's PIE-only linker (no moreunexpected e_type: 2), needs no external loader on Termux (no moreCould not find a PHDR), and stays portable across every arm64 core down to a Cortex-A72 (armv8-abaseline viaMAYA_NATIVE_TUNING=OFF). One file runs on glibc, musl, Termux, and 64-bit Pi OS alike. - Release assets no longer silently vanish behind a draft.
gh release viewsucceeds on a draft release, so a draftvX.Y.Zleft by a prior cancelled run (or auto-created on tag push) would absorb everygh release uploadwhile staying invisible — public download URLs 404 and the unauthenticated API omits it, making a green CI run look like it produced nothing. The release job now alwaysgh release edit --draft=false --prerelease=falseright after ensuring the release exists, so a leftover draft can never swallow uploads again.
- One-command release cut (
scripts/cut-release.sh/.cmd).scripts/cut-release.sh X.Y.Z(orcut-release.cmd X.Y.Zon Windows) is now the entire manual release ritual: it bumpsproject(agentty VERSION …)in CMakeLists.txt (the single source of truth), promotes CHANGELOG's[Unreleased]section to a dated[X.Y.Z], commitsrelease: vX.Y.Z, creates the annotated tag, and pushes branch + tag. The tag push triggers.github/workflows/release.yml, which builds every binary + OS package and submits to winget/homebrew/scoop/AUR (nix/snap/gentoo manifests attached to the release) with zero further input. Guards refuse a downgrade, a duplicate version, a dirty tree, or an existing tag;--dry-runpreviews the exact diff without writing anything,--no-pushstops after the local commit+tag. The Windows.cmdwrapper runs the POSIX script through Git-Bash (or WSL as fallback).
- Ctrl+G now runs PowerShell and cmd blocks natively on Windows. The code-block runner is platform-aware: a
```powershell/pwsh/ps1block is executed throughpowershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand(the body is UTF-16LE-base64-encoded, so arbitrary quoting and multi-line scripts survive thecmd.exewrapper intact), acmd/bat/batchblock and bare fences run throughcmd.exe, and on POSIXsh/bash/zsh/shell/console/terminal(and bare fences) still go to/bin/sh. A block in a language the current platform can't run (e.g.powershellon Linux) no longer masquerades as runnable — Run shows a toast and edit/copy stay available. The Run gate, the runnable-block nudge counter, and the runner all consult oneshell_for_language()classifier so they never disagree. This means the install commands agentty itself suggests on Windows (scoop, winget, PowerShell one-liners) are one keystroke away from running. - The aarch64 Linux binary builds on a native ARM64 runner — minutes, not an hour+. The release build used
ubuntu-latest+docker --platform linux/arm64, which ran the entire C++26 compile under QEMU emulation (~10-20× slower; a single release could sit at 1h+ and occasionally stall). It now runs on GitHub's nativeubuntu-24.04-armrunner (free for public repos), so aarch64 finishes in roughly the same wall-clock as x86_64 — no emulation. Every downstream package that pins the arm64 binary's checksum (Homebrew, AUR, nix) is unblocked in minutes instead of hours. The standalone build is still portable (armv8-abaseline viaMAYA_NATIVE_TUNING=OFF), so a Graviton/Neoverse-built binary runs on any arm64 down to a Cortex-A72.
yay -S agentty(and any SHA256SUMS consumer) no longer 404s on a fresh release. Thechecksumsjob that publishesSHA256SUMSwas gated behind every build including the slow aarch64 leg, so for that whole window the release had noSHA256SUMSasset — and the AUR PKGBUILD, which verifies the downloaded binary against it, aborted with a 404.SHA256SUMSnow publishes in an early pass gated only on the fast x86_64/macOS/Windows legs (a secondchecksums-finalpass refreshes it once the remaining arches land), so the file is present within minutes. Combined with the native-ARM aarch64 build above, the gap it was papering over is largely gone anyway.
- Installable via every major Linux package manager. New packaging manifests for Alpine (
apk add agentty), Nix (nix-env -iA agentty), Snap (snap install agentty), and Gentoo (emerge agentty) join the existing Debian/Ubuntu (apt-get), Arch (pacman/AUR), Fedora/RHEL/openSUSE (dnf/yum/zypper) and macOS/Windows (brew/scoop/winget) targets. Every manifest is a template with the version rewritten from the singleproject(agentty VERSION …)line in CMakeLists.txt at release time — no hardcoded versions anywhere. Thereleaseworkflow now auto-publishes to the Homebrew tap, scoop bucket, and AUR (each gated on a secret, skipped when absent), builds the.apk, and attaches pinned nix/snap/gentoo manifests to the release. Seepackaging/README.md. - Image paste over SSH with zero remote setup (kitty). Ctrl+V on a remote agentty session now pulls a screenshot straight off your local clipboard through the terminal itself: when every host-side probe comes up empty, maya asks the terminal for its clipboard — under kitty it now speaks OSC 5522 (kitty's multi-format clipboard protocol), whose reply carries real image bytes (PNG/JPEG/WEBP/GIF chunked base64, reassembled into one paste; image outranks text when both are on the clipboard; EPERM/EBUSY/ENOSYS abandons silently, like a terminal that never replied). Every other terminal keeps the OSC 52 read — text-only by protocol, so on iTerm2/WezTerm/foot/Ghostty images over SSH still go via
AGENTTY_CLIPBOARD_CMDoragentty airgap --clipboard-relay. Works on every platform on both ends — the bytes ride the pty, no wl-paste/xclip/pngpaste/PowerShell on the remote. - Ctrl+←/→ also quick-cycles threads. Same deck order as
Alt+←/→(← newer, → older), and now on the welcome-screen shortcut row. Fires only while the composer is empty and no agent turn is running — with text in the box Ctrl+arrows stay jump-by-word, and mid-turn the keys fall through to the composer so a live stream can never be yanked out from under you.
- Diff cards no longer render as garish full-bright green/red rows over SSH. Two compounding maya bugs: (1) color detection was too conservative — SSH doesn't forward
COLORTERM(it's not in sshd'sAcceptEnv), so a remote session fell to 16-color ANSI and the dark saturated diff-row bands quantized to solid bright green/red/blue blocks with washed-out text. TERM-based detection now recognizes truecolor terminals that identify viaTERM(kitty, ghostty, wezterm, alacritty, foot, iTerm, konsole,-directvariants) and treats anyxterm-*/screen*/tmux*as 256-color-capable. (2) Even on genuinely 16-color terminals (vt100, linux console) there's now a graceful floor: the write/edit/git-diff previews drop the background bands entirely and fall back to the classic fg-colored diff (green/red+/-text, blue@@headers) — exactly whatgit diffitself looks like there. Override withMAYA_COLOR=truecolor|256|16. - Streaming markdown reveals uniformly across every block type — code blocks, tables, lists, headings all glide like prose. Structured blocks used to pop in whole the instant they completed: a finished code fence / table / list committed to the widget's static prefix immediately, and the typewriter cursor was snapped forward past it — teleporting up to ~200 cells onto the screen in one frame while surrounding prose typed out at ~2–6 cells/frame (measured by the new
reveal_smoothness_probe). Now (maya) block commits are gated on the reveal cursor: a completed block stays in the live tail — rendered in its exact committed shape by the canonical tail path, so zero cells change at the eventual commit — until the typewriter has swept it, then commits via a pending-boundary ledger (each boundary the scanner discovers is committed individually as the cursor crosses it, keeping the live tail bounded to ~one block + the cursor's lag window, so long-turn per-frame cost stays flat). The reveal overlay's live-copy cache also grew an incremental prefix-growth arm (O(new blocks) per commit instead of O(turn)). Verified end-to-end: scrollback oracle (all shapes, zero corruption), reveal_scrollback_test (10 970 checks), full maya suite, CommonMark 652/652. - One-frame phantom blank row inside a streaming code fence. maya's markdown engine treated a trailing
\nas opening an empty final line, feeding a phantom blank content line into any still-open leaf — an unclosed code fence gained a bogus bottom row that appeared and vanished at every line step of the reveal (a height oscillation the monotonicity gate flags). A trailing newline now terminates the last line (cmark §2.1) instead of opening an empty one; spec conformance stays 652/652.
- Esc no longer quits the app. Quit is
Ctrl+Conly. Esc keeps all its useful jobs — cancel a streaming turn, reject a permission, close any modal — but a stray press on the main screen is now inert. This also makes Alt-emulation on iPhone terminals (iSH, Termius, a-Shell) safe: their Alt+←/→ is an Esc-prefix chord, and the Esc half must not be a live exit key. - New CI gate
reveal_smoothness_probe: streams a mixed doc (prose/heading/code/table/list/quote) through the production reveal config on the virtual anim clock and fails on any height shrink or any single frame revealing >60 content cells.
- Run code blocks from replies (Ctrl+G). The picker lists every fenced block in the newest assistant reply (first-line preview · language · line count);
Enteror a bare digit runs one interactively on the real terminal — the TUI suspends via a new maya suspend primitive, so sudo password prompts work, output streams live, and Ctrl+C kills the command (never agentty; classicsystem()signal semantics). stdout+stderr are teed: everything hits the screen live AND lands in a capture (capped 2 MB). On exit a Result card shows the command, exit code, and the full scrollable capture —aattaches it to the composer as a collapsed Output chip (the same collapse/expand machinery as a big paste; expands on the wire as "I ran: … output: …"),ycopies it clean,Esc/Enterdiscards. The composer never receives output you didn't explicitly ask for. Extraction strips uniform$/>transcript prompts (never#comments), tolerates~~~fences, CommonMark indent, and unterminated fences; non-shell blocks offer edit/copy instead of run. Also in the command palette as Run code block. Windows degrades to the non-interactive captured runner. Seedocs/RUN_CODE_BLOCK.md. - Runnable-block nudge. When a reply settles and contains shell blocks, a transient toast ("▶ N runnable code blocks — Ctrl+G to run") surfaces the affordance while the commands are still on screen. Counts only runnable (shell-ish) blocks — a python-only reply stays quiet.
- Thread quick-cycle (Alt+←/→). Flip to the adjacent thread without opening the picker — recency order, wraps at both ends, with a "thread k/N · title" toast on every hop so you always know where you landed. Gated on an idle session; the departing thread is saved first. The
^Jthread list now opens at the current thread (not row 0), marks it with a bold●, and shows the same k/N position readout — both navigation surfaces speak one coordinate system.
- Status bar: the tok/s sparkline now shows from ~90 columns (was 110).
- The remaining streaming scrollback-corruption classes — closed, oracle-proven. A new in-repo scrollback oracle (a pixel-exact terminal-emulator harness that replays full streamed turns and diffs every committed row against ground truth) flushed out and proved fixes for every class it found: (1) trailing prose duplicated above tool cards — the paragraph rode maya's inline tail path (different wrap than the committed-block path) until settle rewrote its already-committed rows; the widget now finishes the instant a tool call exists; (2) frozen-front trim committed an estimate of dropped rows instead of the exact count; (3) tool-card grow while overflowed strand-painted rows into scrollback (maya grow-guard + reconcile cooldown); (4) chrome strands — a committed drop larger than viewport+margin preserved a pre-commit canvas and smeared composer/status chrome into history; (5) shrink-while-overflowed at stream settle duplicated the composer one screen up (maya's verify-poison recovery committed rows unconditionally). The oracle passes all shapes with maya's scrollback-invariant gate instrumented and zero gate firings — the gate exists, but nothing trips it.
- Scrollback no longer wiped by gate recovery. The scrollback-invariant gate's grow recovery arm demoted to a hard reset whose escape sequence (
\x1b[3J) deleted the terminal's entire native scrollback — you'd suddenly see only the last few turns. The wipe dated from an era when recovery re-serialized from row 0 with bottom-edge scrolls; today's repaint is viewport-capped, so the destruction was pure loss. Grow recovery now commits off-viewport rows and soft-repaints, same as shrink — no reachable render path clears scrollback anymore (only width-change resize, hard write failure, and explicit thread swap). write/editstreaming cards: seam-stable, no collapse, no committed-row rewrites. Long streaming edits used to balloon the card (every hunk rendered), tick already-committed "/N" headers, and could collapse the card to zero rows mid-stream. The streaming preview now feeds all hunks to maya's diff widget, which pins a status chip and windows the visible body to a cross-hunk row tail; the header/detail are lifecycle-stable (no Done-only suffix rewriting committed rows); the body budget adapts to content instead of pinning worst-case height; and the chip shows the landing-hunk ordinal while streaming.- Live tool panel animates without touching the freeze seam. The in-flight agent-timeline panel's spinner moved to a seam-safe footer so animation frames can't perturb rows above the live/frozen boundary.
- Model catalog recognises the Claude Fable/Mythos flagship lane.
- Test-suite wall clock: 280 s → 24 s. maya's animation system (reveal effect, activity indicator,
anim::Clock/Mount) now reads a single skewable time source (anim_now_ms(): steady clock + test-only additive atomic skew). Tests advance the clock instead of sleeping 20 ms per frame; production cost is one relaxed atomic load.
- Scrollback duplication / ghosting across streaming, height-grow, compaction, and narrow viewports — the whole class, closed. A cluster of inline-render bugs could leave stale or duplicated rows in the terminal's native scrollback when the live tail transitioned to a frozen (immutable) block. Fixes: the freeze gate (
live_tail_reveal_settled) now mirrorsbuild_live_tail'sis_live()hash-stamp condition so the two seam gates can't drift; frozen-measure width subtracts the full 4-column chrome (AppLayout+Conversationpadding), not 2, so narrow terminals freeze at the same width maya renders; the case-B height-grow cursor anchors to full content height; the compaction divider is now emitted symmetrically in the live tail so the freeze seam stays height-stable across a compaction; and frozen-trim commits a conservative under-estimate of scrolled rows (never the exact count) — an over-commit re-scrolls the visible tail, but an under-commit is reconciled by maya, mirroring the provenagent_sessionbehaviour. Atfps=0, markdown now finishes immediately andvisual_hashis driven through the settle cooldown so no duplicate paints slip through. Each fix ships with a regression test. - Streaming reveal no longer bursts, stalls, or scrambles on long turns. The maya reveal effect glides tables and code blocks left-to-right with a commit-safe seam (no eager scramble of not-yet-typed cells), holds final table column widths during reveal (no horizontal reflow), and reveals the newest table row with the same positional comet gradient as prose. Tall tables no longer ghost-duplicate into scrollback.
- Data race on
provider::active(). A worker thread read the active provider while the UI move-assigned it, tearing the read. The value is now snapshotted under the mutex. - Memory tool: garbage rollover count + silent scope downgrade. The rollover counter could print garbage, and a
remembercould silently downgrade its scope; both are fixed (the scope downgrade is now refused).
- Inline render is bounded to the viewport window for tall transcripts. A giant frozen or streaming block used to pay O(content-height) every frame (full-canvas clear + a memcpy-per-row blit). The paint path now clears only the rows below the immutable prefix (
clear_below, gated on a Synced coherence state, no canvas realloc, and an 8-row margin above the viewport) and skips the per-row blit when the destination is already byte-identical to the source (blit_packed_row_cachedvia SIMDbulk_eq). Single-authority scrollback accounting (overflow = prev_rows − term_h) is fully preserved — the worst case is a perf non-improvement, never corruption. Steady per-frame cost on tall blocks drops ~30%. - Tool-diff bands: GitHub-dark styling.
write/editdiffs render dark-but-saturated green/red backgrounds with bright same-hue text and a sign rail, readable as green/red (not gray) even on low-gamma panels; clean single-filename header, no git plumbing. - Responsive status bar. The CTX gauge is lowest-priority (drops first, desktop widths only); the provider badge shows from ~50 columns; compact CTX (bar graph + percent) shows from ~40 columns so phone-width terminals keep the fill graph and %, with raw token counts only on wide terminals. Picker footer hints drop responsively to fit.
- Diff is trimmed-LCS, not O(N·M). Common prefix/suffix are trimmed before the LCS, the SSE debug gate is lock-free, and a redundant stream-sink hop was dropped.
- Off-screen giant message bodies collapse on rehydrate (default off — it was hiding loaded messages, now opt-in), and settled tool panels are cached in long in-flight turns.
- Build auto-pulls all submodules (maya, acp-cpp, mcp-cpp) to latest.
agentty airgap <host> --acp [flags…]— one-command Zed-over-airgap setup. Running agentty inside Zed on an internet-less remote used to mean hand-assembling assh -N -R 1080tunnel plus a Zedenvblock. The new--acpform prints a ready-to-paste Zedagent_serversconfig (and the path to yoursettings.json) whosecommandissshitself — its args open the reverse SOCKS5 tunnel and exec the remoteagentty acpin a single invocation, with the ACP JSON-RPC riding ssh's stdio. One ssh process is the tunnel, the agent, and the transport; Zed owns its lifecycle, so there's nothing to babysit. Everything after--acp(e.g.-m,--profile,--workspace,--sandbox) is forwarded verbatim to the remote agent. Pair with--setupto copy credentials over first.agentty acpnow supportssession/load— resume past conversations in Zed. The ACP agent advertisesloadSession: trueand persists every session to agentty's on-disk thread store (threads_dir()/<id>.json, the same format the TUI writes) after each turn, so sessions survive a subprocess restart. Onsession/loadit restores theThread(preferring an in-memory copy when the session is still live in this subprocess, else reading from disk), replays the entire conversation — user messages asuser_message_chunk, assistant text asagent_message_chunk, and each tool call as atool_callcard with its final input/output/status — assession/updatenotifications, then resolves the request, exactly per the ACP spec. Session ids are realThreadIds, so ACP sessions also appear in the standalone TUI's thread picker (and TUI threads are loadable from Zed). Fixes the "Loading or resuming sessions is not supported by this agent" error.agentty acpACP refinements: model + permission-profile flags, file follow-along, faster cold start.-m / --modelis now an ephemeral per-subprocess override in ACP mode (it no longer clobbers the TUI's saved model), so a Zedagent_serversentry can pin a fast model (e.g.claude-haiku-4-5) without touching your interactive default. A new-p / --profile {ask|minimal|write}flag tunes which tools trigger Zed's permission prompt:ask(default — prompt write/exec/net, auto-run reads),minimal(prompt everything including reads),write(never prompt reads). Tool calls now carry ACPlocations(file path + optional line) for read/edit/write/list_dir/git_diff/diagnostics, enabling Zed's "follow-along" file highlighting. ACP mode now prewarms the TLS/DNS connection to Anthropic before serving (matching the TUI), eliminating the ~150–300 ms handshake on the first prompt, and the wire tool list is built once instead of per-completion.agentty acp— run agentty as an ACP agent inside Zed (or any Agent Client Protocol client). A new headless subcommand speaks newline-delimited JSON-RPC 2.0 over stdio and implements the full ACP v1 agent surface:initialize(capability negotiation),authenticate,session/new,session/prompt(drives a complete agent turn), andsession/cancel. While a turn runs it streamssession/updatenotifications —agent_message_chunkfor model text,tool_call/tool_call_updatefor every tool (with ACPkind,rawInput, status transitions, anddiffcontent blocks for edit/write so Zed renders changes inline) — and calls back withsession/request_permissionbefore any side-effecting tool runs (Exec/WriteFs/Net), letting Zed show its native approval UI. The headless loop reuses the exact same provider, tool registry, wire-message shaping, workspace sandbox, and permission policy as the TUI (no maya/UI dependency), so behaviour is identical to interactive agentty. Configure in Zed'ssettings.jsonunderagent_serverswith{ "command": "agentty", "args": ["acp"] }; auth comes from your existingagentty login. See README → “Use agentty inside Zed (ACP)”.
- Per-error-class retry caps (Zed-aligned) so a flaky mid-stream wire stops spamming the retry banner. Previously every transient shared one global
kMaxRetries(6), so a connection that kept cutting out mid-body stuttered through six loudtransient — retrying (attempt N/6)…banners before giving up. Mirroring Zed's agent loop (crates/agent/src/thread.rs::retry_strategy_for), the cap is now per error shape viaprovider::max_retries_for: rate-limit / overload (429 / 529) keep the full budget (the server is shedding load and usually hands aRetry-After), a clean connect blip with no content keeps the full budget (a fresh connection almost always recovers), but a mid-stream failure — the stall watchdog fired, or the stream had already delivered a delta this turn and then died — gets only 2 attempts, because a wire that keeps dropping after reaching us is a real outage, not a reconnect artifact. The attempt counter in the banner now shows the real per-class cap (N/2mid-stream,N/6otherwise). Budget-decay and the first-delta/heartbeat budget reset still apply on top, so a stream that recovers and runs for a while resets to a fresh ladder. x-stainless-retry-countnow reflects the real attempt number. It was hard-coded to0, so every retry looked like a fresh first attempt to Anthropic's edge — which reads this header for routing and to avoid penalising retried traffic, and could land a retry back on the same overloaded pop. The per-turntransient_retriescount is now plumbed throughprovider::Request→anthropic::Request→ the header, exactly as the official SDK / Zed increment it.- Frequent
transient — retrying…banner caused by stale pooled connections. The dominant trigger for the orange retry banner was a reused h2 connection that Anthropic's edge (or an intermediate proxy) had silently half-closed: the pool's acquire-time liveness checks (nghttp2protocol state + a non-blockingMSG_PEEK) passed because the FIN/GOAWAYwas still in flight, so a corpse got handed out. The new stream submitted on it was immediatelyRST_STREAM'd /GOAWAY'd — and because the old retry gate (any_bytes, set on headers) considered a headers-only:statusblock "committed," the HTTP layer couldn't re-dial. The error bubbled to the reducer, which restarted the whole turn loudly with backoff. Two transport-layer fixes: (1) the stream-commit point is now real SSE DATA (on_chunkwith body bytes), not headers — a stream that got only:statusbefore the reset is replay-safe and re-dials transparently; (2) a reused pooled connection that dies before delivering any data gets up to 2 free fresh re-dials that don't count against the transport attempt budget, so a pool-staleness artifact never surfaces as a user-visible error. Genuine fresh-dial failures and any reset after real content still converge to a terminal error exactly as before. This is what the official Anthropic SDK / Claude Code get for free from undici's managed pool (honorGOAWAY, retry transport resets on a fresh connection); agentty now matches it. - Transient backoff that never recovered + frequent "stream stalled" after long sessions.
transient_retriesonly reset to 0 on the first content delta, so a stream that connected, sent heartbeats, then went silent before any byte (common during brown-outs and long opus turns) climbed the retry ladder every attempt until it hitkMaxRetriesand latched terminal — the session was dead until restart. Two fixes: (1) a heartbeat (SSEping/thinking_delta) now resets the retry budget too, since it proves the wire is alive even pre-content; (2) the budget decays over wall-clock time — if the previous failure was longer ago thankRetryDecayWindow(90 s) the connection was healthy in between, so the new failure starts a fresh ladder instead of inheriting an unrelated earlier blip. Net effect: fast-failing connections (refused/reset within 90 s) still converge to terminal at attempt 6, but slow stalls minutes apart recover indefinitely.Escstill breaks the loop at any point.
--version/-V/versionflag — printsagentty <PROJECT_VERSION>and exits. The version is baked at build time fromCMakeLists.txt'sproject(... VERSION ...)line, so bumping the project version updates every site that readsAGENTTY_VERSION.- Queued messages render as preview rows in the conversation transcript (above the composer), visually identical to real user turns. Mirrors Claude Code 2.1.119's behaviour at binary offset 80106500.
↑(Up-arrow) on an empty composer recalls every queued message back into the buffer, joined by\n, with the cursor at the recalled-text seam. Destructive on the queue — re-submit to re-queue. Mirrors Claude Code'sLc_(offset 76303220).- Composer placeholder gains a
press ↑ to edit queued — type to queue another…hint when the queue is non-empty and the buffer is empty (and matching variants for awaiting/idle phases). Mirrors Claude Code's hint at offset 84591379. - Retry status now shows attempt counter:
transient — retrying in 5s (attempt 2/6)….
- Transport reliability. Anthropic's
Retry-AfterHTTP header is now parsed on 429 / 529 responses and used as the authoritative backoff delay, clamped to[1s, 120s]. Falls back to the existing 500ms→45s ladder when no header is present, with ±20% jitter applied to break thundering-herd retry sync during regional brown-outs. Inspired by Zed'sparse_retry_after(crates/anthropic/src/anthropic.rs:574-580). - Cancel cleanup.
Escnow does the full teardown synchronously: drainsstreaming_textintotext(preserves partial reply), marks every non-terminaltool_callasFailed("cancelled"), pops the assistant placeholder if it produced no content, and resetspending_permission. No more orphanRunningspinners or empty placeholder cards after cancel. - Status banner row replaced by a notification takeover on the existing shortcut row — when
m.s.statusis active, the keybindings strip swaps in a single banner-style entry (▎⚠ <text>for errors,▎ <text>for info) and reverts to bindings when the toast expires. No new rows added. submit_messagenow queues on any non-Idle phase (m.s.active()) instead of justis_streaming() || is_executing_tool(). Defensive — the keymap already gatedAwaitingPermissionvia the permission modal — but makes the guarantee structural.
- Model / thread / palette pickers felt unresponsive — arrow keys "registered once per 4-5 presses." The Program render gate (
visual_hash) didn't include any modal/picker selection state, so moving the cursor (ModelPickerMove→index++) produced a model the gate considered visually identical andskip_renderfired; the new cursor position only painted when an unrelated hashed axis (the ~265 ms composer caret-blink parity) happened to flip.visual_hashnow mixes in every modal's open/closed state plus the active picker's cursor index and filter query, so each keystroke repaints immediately. - Picker arrow keys double-dispatched. The picker
ScrollStates defaulted toauto_dispatch = true, so every ↑/↓/PageUp was fed intoScrollState::handle(bumpingscroll.y) in addition to the reducer's selection move — the two then fought the widget's selection-follow clamp. Setauto_dispatch = falseon all six picker scroll states; scroll position is now a pure function of the selected index. - Up-to-100 ms input stall on bare Escape and split escape sequences (maya). The idle (
fps=0) event loop slept the full 100 ms poll while the input parser held a partial escape sequence (a lone ESC, or an arrow key whose bytes arrived in separate reads over SSH/tmux/slow ptys) — onlyflush_timeout()could resolve it, and only after the 50 ms escape deadline, but the loop never woke to call it. The loop now clamps its poll timeout to the escape deadline while the parser has pending input (Runtime::has_pending_input()). Most visible in the pickers, which idle with no spinner tick to keep the loop spinning. agentty gets stuck — nothing worksafter Esc. A worker thread's trailingStreamError("cancelled"), dispatched ~200 ms after the cancel-token trip, was running on the runtime'sactive_ctx. If the user submitted a new turn during that window, the handler'sa->cancel.reset()would null out the new turn's cancel token, leavingEscunable to cancel anything until process restart.launch_streamnow wrapsdispatchin aguardedlambda that captures the cancel token and short-circuits when tripped — no events from a cancelled worker reach the reducer, so the new turn's state is never touched.- Removed the redundant
N messages queuedline from the shortcut row; the composer's own❚ N queuedchip is now the single source of truth for queue depth.
Pre-1.0. Core loop, tools, streaming, permission profiles, in-app auth, persistence, and cross-platform subprocess all working. Linux gets daily smoke testing; macOS and Windows code paths exist (#ifdef branches throughout, posix_spawn for POSIX, CreateProcessW for Windows, fdatasync/fsync switched per OS) but CI for those platforms is next.
- Native C++26 TUI rendering through the
mayawidget engine (sister project, FetchContent-pulled from1ay1/maya). Single ~9 MB static binary, no Node / Python / Electron runtime. - Anthropic provider speaking HTTP/2 + SSE directly via in-house
nghttp2+ OpenSSL stack. OAuth (PKCE) + API key both wired through the sameauth::cmd_loginpath. - Tools:
read,write,edit,bash,grep,glob,list_dir,find_definition,web_fetch,web_search,todo,diagnostics,git_*. Compile-time effect set + permission policy enforced viastatic_asserton aconstexprmatrix. - Permission profiles:
Write(autonomous),Ask(read-only auto, write/exec/net prompt),Minimal(only pure tools auto). Profile cycle onS-Tab. - Sandboxed bash by default —
bwrapon Linux,sandbox-execon macOS. Windows runs unsandboxed (no first-class equivalent yet). - Workspace boundary: filesystem tools refuse paths outside
--workspace/cwd. - SSH air-gap mode (
agentty airgap …): wraps agentty on a remote host with SOCKS5 forwarding for TLS / OAuth / chat traffic. Compression off by default (small bursty deltas not worth zlib sync overhead on inline frames); env vars for terminal identification forwarded across the SSH boundary so DEC 2026 sync still applies on the remote side. - Persistence: threads and credentials in
~/.agentty/threads/and~/.config/agentty/credentials.json(mode 0600). Atomic writes (temp + fsync + rename). - Streaming smoothing: SSE deltas drip into
streaming_textat ⅛ buffer per Tick (clamped 32–256 chars), so server-side batching doesn't translate into chunky on-screen text. - Inline rendering — agentty never takes over the terminal; output flows in scrollback, status bar overlays.
compose_inline_framewraps frames in DEC 2026 begin/end-sync where supported.
- Checkpoint restore —
CheckpointId+ per-message marker exist;RestoreCheckpointsurfaces "not implemented yet" and does nothing. - Diff review pane — modal renders, but
pending_changesisn't populated by any tool yet, so review/accept/reject toasts "no pending changes".
- C++26 (GCC 14+ / Clang 18+); MSVC builds against
/std:c++latest. - AppleClang tops out at C++23 —
AGENTTY_BUILD_TESTSrequiresg++or stock LLVMclang++on macOS, not Xcode's bundled toolchain. cmake -B build && cmake --build build.AGENTTY_STANDALONE=ONproduces a static binary (libc and usually OpenSSL stay dynamic).