Changelog
All notable user-facing changes to cast, newest first.
Unreleased
0.36.2
Fixed
- A code review could not correct findings the position check rejected. The first
review_reportclosed the review, so a model told that its findings were unlocatable had nowhere to send the corrected ones — it got "no code review is open". Found on a real 45-file review where all four findings were dropped as unlocatable, which is exactly the case the check exists for. The scope now stays open until the turn ends, however many submissions that takes.
0.36.1
Tests only: the TUI /code-review route shipped without one in 0.36.0, which the per-file coverage floor caught after the tag was cut.
0.36.0
Added
/code-review [range] [-- path…]— a review of a change, where the scope is computed rather than recalled. Which files are in it, which are filtered as generated or vendored (each named with its reason), how they group into review units, and which language rules apply are all decided in code before the model sees anything; only the judging is left to it. Rules live inprompts/review-rules/, one document per language, loaded only for the languages actually in the diff — add your own by dropping a file there. Findings go through areview_reporttool that checks each against the file: a wrong line is moved to where the quoted code actually is, a finding whose code isn't in the file (or whose file is out of scope) is dropped before you read it, and one on an untouched line is kept and flagged as context.-- <path>narrows a change too large for a single turn; past 25 files the command says so up front.
0.35.0
Changed
- A goal's first "complete" now has to prove itself.
goal_updateanswers the first completion with a demand for evidence — enumerate what the objective requires from the current state, inspect each requirement, then call again with what you saw — and closes on the second call. One challenge per goal, so nothing can be held hostage by its own check, and an agent that stops at the challenge leaves the goal active rather than closed. Measured on MiniMax-M3 over twenty runs of a goal whose objective covers three files while the prompt names one: 19/20 before, 20/20 after; the run that failed had closed the goal after the first file. The check costs one extra model round per completion.
0.34.1
Fixed
- The
bash_outputwaitargument was still being written in seconds. Both duration arguments advertised that a value under 1000 would be read as seconds, which reads as permission rather than a warning — so a model that had just writtentimeout: 120000correctly went on to writewait: 2for two seconds. The leniency stays in the runtime; the schema now teaches by example instead ("for ten minutes pass 600000", "for two seconds pass 2000"). Measured on MiniMax-M3 before and after:waitnow arrives as 2000, and a half-hour timeout as 1800000 rather than 1800 — the value that is legal milliseconds and would have silently become 1.8 seconds.
0.34.0
Changed
bashandsshtimeouts, andbash_output'swait, are now in milliseconds (default180000, max3600000). The unit was seconds, and a model that had learned the other convention senttimeout: 120000meaning two minutes — read as 33 hours, which is not a long timeout but no timeout at all, leaving a hung command to hold the turn. Milliseconds is the more common convention across harnesses, so that is what the tools take. A value under 1000 is read as seconds and converted, with a warning naming the unit, so the opposite mistake can't turntimeout: 600into a 0.6-second deadline. Above the maximum the value is capped, with a note that longer work belongs in the background. This is breaking for callers that passed 1000 or more meaning seconds:timeout: 1800now means 1.8 seconds, not half an hour.- A
bashrow in the TUI shows the timeout that will apply to it —bash npm test · 3m— so the deadline is visible while the command runs rather than only in the[TIMED OUT]result afterwards. It is the effective value, read through the same code the tool uses, so a converted or capped timeout displays as what will really fire; a background task that asked for no timer shows nothing.
Fixed
- A failed checkpoint write no longer dumps the validator's report into the transcript. The whole issue list was the error message and the error message is the user's notice, so a dozen lines about section budgets and missing
Why:entries landed mid-turn. The notice is one line now — what blocked it, that the checkpoint on disk is untouched, and where the full list is written beside the rejected draft. It quotes a blocking issue rather than the first one, since warnings ride along in the same list and never caused the failure. The writer also gets three passes instead of two, because one repair pass rarely fixes an oversize document and several over-budget sections at once.
0.33.1
Added
- A
bashrow in the TUI shows the timeout that will apply to it —bash npm test · 3m— so the deadline is visible while the command runs rather than only in the[TIMED OUT]result afterwards. It is the effective value: the call's owntimeoutwhen it gave one, the foreground default when it did not, and nothing at all for a background task that asked for no timer, since nothing will stop it.
0.33.0
Changed
- Reasoning blocks are hidden by default, in the TUI and the web UI alike. Thinking is the model talking to itself, and on a reasoning model it arrives in bulk and buries the answer. Turn it on with
/reasoning-display(/rd) or the web Settings > Appearance switch; the choice persists. AshowReasoningalready written insettings.jsonis left alone — this changes only what happens when nothing has chosen.
0.32.1
No behaviour changes — this release exists so the published version matches the tree CI validates. 0.32.0 shipped with the durable-goal paths in loop.ts and commands.ts untested, which the per-file coverage floor caught after the tag was cut, and with search.ts coverage that depended on whether fd was installed on the machine measuring it. Both are covered now.
0.32.0
Added
/goalnow outlives the turn that started it. The objective is recorded for the session and rides along with every later turn until it is closed, instead of living inside a single submitted message. Where a turn would end, an open goal continues the run on its own — bounded by a continuation budget and by the turn's existing iteration cap — and a pass that changed nothing gets told so rather than repeating. The agent closes the goal with a newgoal_updatetool, which requires the evidence for every requirement the objective names; a blocker only ends the goal once it has come back three times, and a safety or policy refusal ends it at once./goal status,/goal edit <text>and/goal clear. Status and clear work while a turn is running, which is when a long autonomous run most needs them. Editing rewords the objective without resetting the history behind it.
Fixed
- A session lock could be taken from a live, working process. The turn-runner lock recorded when the turn began and nothing refreshed it, while anything older than 60 seconds counted as stale — so an ordinary long turn was stealable and two agent loops could drive one session. The lock now heartbeats while the turn runs, so "stale" means the owner died or wedged. The same staleness also made a long, healthy turn show up as idle in the web UI.
0.31.7
Fixed
- The
coder-with-subagentspersona demonstrated aneditcall that doesn't exist. Its Validate-then-Commit example passedpathand aneditsarray; the tool takesfilePath,oldStringandnewString. The example now matches the real signature. - It also carried an instruction that contradicted the shared file-tool rules, telling the agent to use one
editcall per changed place while the shared workflow puts adjacent changes in a single call and splits only unrelated regions.
Changed
coder-with-subagentsno longer repeats the shared appendix. Its Guidelines and Working Style sections restated the tone, file-tool, verification and preamble rules that every persona already receives. What stays is what those sections don't cover: reuse before writing new code, read a file fully before wide-ranging changes, ask when a requirement is unclear, and report which subagents were spawned. The persona goes from roughly 5100 to 4650 tokens.
0.31.6
Changed
- Read-only subagents no longer carry the
editargument contract.exploreandreviewdeclare tool allowlists withouteditorwrite, but their system prompt still spelled out the wholeoldString/newStringcontract — about 320 tokens of rules for a tool they cannot call. The shared appendix now takes the agent's allowlist into account. The file-tool workflow above that section is unchanged: it governsread/grep/glob/ls, which every agent has. - The agent is told not to re-read a file whose content it already has, unless it edited the file, something else may have changed it, or an error suggests its copy is stale.
- Five personas no longer repeat the
skilltool bullet. The skills instructions already explain the tool in full, and they are appended only when skills exist — with none installed, the bullet described a menu with nothing on it.
0.31.5
Changed
- A
grepresult with three matches or fewer now says to read the file before describing the code. A grep line carries the matched text, so it reads like a finished answer, and the model went on to report on code it had never opened.globalready gives the same nudge on a short hit list. - The "inspect with the tools, not with
bash" rule now covers verification too. Reading back a file you just wrote goes throughread, notcat/wc/xxd, and several inspections packed into onebashcall are called out: the blocks of output then have to be matched back to the paths that produced them, and getting that backwards reports the opposite of what is on disk.
0.31.4
Fixed
globreported "No files found" for an absolute pattern whose files were right there. With a pattern like/abs/dir/**/*.spec.tsand nopathargument, the search still ran from the current directory, while the/in the pattern switched on full-path matching and anchored it with a leading**/— so the pattern being matched was**//abs/dir/**/*.spec.ts, which can never match. An absolute pattern now takes its own directory prefix as the search root. The miss was silent: no error, just an empty result the model had no reason to distrust.- Three prompt rules removed in the 0.31.3 prompt trim are back. The trim described them as consolidated elsewhere, but nothing carried them: no colon before a tool call, short progress updates at key moments rather than silence between the opening preamble and the summary, and a preamble on every response that calls tools (not only the first of a turn).
0.31.3
Changed
pptxgenjsis now a declared dependency in cast's ownpackage.json, tracked alongside cast's other dependencies instead of being an untracked package onlynpm audit/lockfile tooling couldn't see. Thepresentation-builderskill generates and runs its build script inside the target project's directory, so this does not remove the need fornpm install pptxgenjsthere — cast's own dependency list isn't on that script's module resolution path.
Fixed
/distillcould write a generated skill or persona outside its intended directory. The name sanitizer for a model-proposed artifact stripped disallowed characters but let a bare.or..through unchanged, so an artifact named".."landed at.cast/SKILL.mdinstead of.cast/skills/<name>/SKILL.md. Dot-only names are rejected now; the artifact is skipped instead of materialized in the wrong place./distillcould propose acommandartifact that did nothing. It wrote to.cast/commands/<name>.md, a path nothing in cast ever reads back as an invocable command. Droppedcommandfrom the set of artifact kinds/distillcan produce — onlyskillandpersona, both of which are actually loaded.
0.31.2
Added
- The connection dot in the header says what it means on hover:
Connected,Reconnecting…orNo connection. It was a coloured dot and nothing else.
Changed
- Status and Keyboard shortcuts are the same window as Settings — same width and height, full-bleed on a phone the same way, so the three dialogs are one shape instead of three.
- The session's directory is in brackets next to the persona:
SENIOR DEVELOPER (/home/ubuntu/pet/cast).
Fixed
grep(andglob) against a sibling directory returned a path the model could not read. Withpath: ../proj-extra, rg was run withcwd: ../proj-extraand the relative argument., so a hit inb.txtcame back asb.txt:1:hit here— a bare basename the model then couldn'tread. The directory search path is now resolved to an absolute root, rg is run with that as its cwd, and rg's output is rewritten so every path is absolute again. The JS fallback already produced absolute paths and is unchanged.- "Load more" in the sidebar looked like it ignored the click. The button had no pending state at all: the next page of sessions appeared 200ms later on a fast link and well past half a second on a phone, with nothing happening in between. It now shows a spinner and refuses a second click while the page is on its way — measured at 27ms from the click, whatever the latency. While it spins, the button drops its frame: there is nothing to press, so only the spinner is left.
- The Status dialog spilled its last rows outside the box. It is a flex column with a capped height, and its body never declared that it may shrink and scroll — so with a long model id, provider name or branch, "Git branch" and "Worktree" were drawn below the dialog, on top of the page behind it. The body scrolls inside the dialog now. The confirm dialog had the same gap and got the same fix.
- A tooltip kept the text the element was born with. The themed tooltips copy an element's
titleonce, so every title that changes with state — the connection dot, Abort while it is aborting, Send while it is sending — showed its original wording forever. The text is re-read each time the pointer arrives. - Restarting the daemon told every open tab "This session was closed". A shutdown closes the sessions it holds in memory, and the browser reported that as an error — the thread is on disk and the page reconnects to it seconds later. The event carries the reason now, and a shutdown is not announced: the status dot already shows the reconnect.
- The first message in a new, empty chat could fail with "Connection lost". A draft has no event stream on purpose — it is created by the very message being sent — and two separate checks treated that as a dead connection: the composer's "can I send?" (so a new chat showed "Reconnecting…" and refused outright), and the wait for the stream after the session was created (so on a slow link the message was dropped back into the composer once the wait timed out). Sending is gated on the daemon answering now, a live stream is required only once the session exists, and waiting for the stream orders the events without ever cancelling the send — the POST is what sends the message, and only its own failure is reported (and retried). Verified with 400-900ms of latency per request and with the event stream held open for 3-5 seconds: the message goes out every time, with no error.
- Sending while the page was reconnecting failed instead of waiting. A phone coming back from a locked screen, or a daemon that just restarted, leaves the page disconnected for as long as the retry loop takes to notice — and a send in that window bounced with "Connection lost", with the typed text only reappearing afterwards. Pressing send now asks for a reconnect immediately (instead of sitting out the retry's 3s sleep), keeps the draft on screen while it waits, and sends as soon as the daemon answers; measured end to end with a daemon restart, the message goes out with no error at all. It gives up after 6 seconds, which is the only case that still reports "Connection lost". The reconnect is also kicked when the browser reports the network back.
- The turn timer appeared a round trip after the message was sent. The composer flips to Abort the moment a send leaves the browser, but the elapsed counter waited for the daemon's
status:runningevent to name a start time: measured at 125ms on localhost and 232ms with 150ms of latency, and longer on a phone. It now starts from the send itself and adopts the daemon's timestamp when it arrives, whichever is earlier, so the reading never jumps backwards. Steering into a running turn keeps that turn's own start.
0.31.1
Fixed
- Mobile: the keyboard closed on send and popped back up mid-request. Tapping Send moved focus from the textarea to the button — the keyboard started sliding away — and the composer only refocused the textarea after
onSubmithad resolved, so the keyboard came back a network round trip later, right as the answer started loading. The composer buttons no longer take focus on press, and the textarea is refocused synchronously inside the tap, so the keyboard simply stays up. Focus also no longer ends up on the Abort button that replaces Send when the turn starts. - Mobile: the keyboard opening left the chat scrolled away from the newest message. The on-screen keyboard takes ~390px off the transcript's height, and losing height fires no scroll event, so the view stayed where it was — 390px above the bottom, with the newest message below the fold. The list re-pins on its own resize now (the same fix covers the workspace panel opening on a desktop). A reader who had scrolled up is left where they were. The page also declares
interactive-widget=resizes-content, so the keyboard shrinks the layout instead of covering it: the header stays put and the composer sits directly on top of the keyboard. - Mobile: the persona above the composer wrapped mid-label and the path ran off the screen. The role line is one line now, and the path is truncated from the left (
…/scratchpad/panel/home/proj1), since the end of it is what names the project. The full path stays in the tooltip, and a wide screen still shows all of it.
0.31.0
Removed
- Pluggable ("factory") UIs are gone. The web UI is served at
/again instead of redirecting to/default/;/default/*answers with a 301 to the same path without the prefix, so old bookmarks keep working. Removed with it:~/.cast/ui/*discovery,/ui,/ui/<name>/,/<name>/,GET|POST /api/uis,/api/uis/events,/api/settings/default-ui, the Default UI settings tab, theactiveUi/defaultUisettings, theui-factoryskill, the UI template shipped in the package, and the write guard that refused agent edits to cast's ownsrc/server/public.
Added
- The session's directory is shown above the composer, next to the persona:
SENIOR DEVELOPER /home/ubuntu/pet/cast. It was visible nowhere in the web UI, so two threads in different projects looked identical. The full path is shown. The line also appears for a session with no persona set, which used to hide it entirely.
Fixed
- Web chat stopped following the stream. Two causes. The scroll handler decided "still at the bottom?" by measuring the position after a render but before the catch-up scroll, so once a streaming frame added more than 80px (the adaptive repaint rate from 0.30.3 batches more per frame) it concluded the reader had scrolled away and stopped following for good. And when the streaming block was swapped for the settled message, that message was laid out as its 120px
content-visibilityplaceholder for one frame, so the list was pinned once and then grew by the real height underneath, leaving the view thousands of px above the bottom. Following now turns off only on an actual scroll up, ignores the clamp that comes with a shrinking list, and aResizeObserveron the transcript keeps the view pinned whatever grows it (a realised message, a highlighted code block, an image). - Web settings: editing the active provider lost the model. The form did
/provider delete+/provider add, and deleting the active provider switches to a fallback, clears the chosen model and drops the subagent/plan slots pointing at it. There is a/provider edit <name> <url> <apiKey>now that updates in place. - Web settings: search API keys no longer travel to the browser.
/web-search-providerreports only whether a Tavily/Brave key is saved; the field starts empty and a saved key is kept when it stays empty. - Web settings: a tab whose data failed to load rendered blank. Model, Bash and Web are built from several commands; when one failed the tab drew empty values with no error. The first failure is shown now. The model pickers also follow the saved values after every reload instead of staying on the last pick.
- Web settings: a rejected command showed no error. The tab reloads its data after every action, and the reload cleared the error slot the action had just filled, so a refused save (
/turn-cap 5, an invalid threshold list) left the tab looking as if it had succeeded. The error is set after the reload now. - The workspace panel no longer stalls the daemon, and keeps its place when you switch threads. Three things, all measured against two projects with 50 and 5 changed files:
GET /api/sessions/:id/diffrangitsynchronously, one process per changed file, on the daemon's event loop. One request on the 50-file project held it for 1–4 seconds, and everything else waited behind it: a session switch took 1.5s instead of 30ms, the file tree 2.5s instead of 10ms,/api/config850ms instead of 3ms — for every client, not just the one asking. The diffs run asynchronously now, ten at a time: 1.06s → 0.27s for the request itself, and the request next to it stays at ~20ms. Paths are passed as arguments instead of spliced into a shell string, so a file with a space in its name diffs too.- The diff was fetched whenever the panel was open, whatever tab was showing — every session switch with Files or Memory in front still ran the full
git difffor nothing, and a quick run through several threads left every earlier request running to completion on the server with its answer discarded. It is fetched only while Changes is showing, and a superseded request is aborted. - Changes showed an empty diff for any file whose name has a space or a non-ASCII character in it.
git status --porcelainwraps such a path in quotes ("has space.ts") and writes a Cyrillic name as octal escapes, and the parser took the string literally, so the follow-upgit diffasked about a file that does not exist. Status is read with-z(no quoting, NUL-separated) andcore.quotePathis off for the diff headers, soфайл.tsisфайл.tson both sides. Renames read their old name from the-zrecord instead of splitting on->. - The Files tree reset on every session switch — folders collapsed, root refetched — even between two threads in the same directory, where the files are the same files. It is keyed by the project now: switching threads within one keeps every expanded folder and makes no request at all; switching projects reloads as before.
- The web transcript renders markdown properly. It was a hand-rolled set of regexes, which got the common cases right and then fell over:
2 * 3 * 4 = 24came out with the 3 in italics,`a **b** c`put a<strong>inside the<code>, every heading level collapsed to a single<strong>, and blockquotes,---, nested lists, task lists,~~strike~~,__bold__,~~~fences,1)numbering and a fenced block inside a list item were not implemented at all (an unterminated fence also stayed raw text once the turn settled, so the view changed under you when it did). marked parses it now — it was already vendored for the file preview, so no new dependency — and every one of those renders correctly. Safety did not regress, it got a second layer: the renderer emits no raw HTML of its own (an HTML token in the source comes back escaped, and a link is only a link if its scheme is http/https/mailto —javascript:anddata:render as plain text), and DOMPurify sanitizes the result on top of that. The cost is 19KB more on the first load, cached forever; time to an interactive composer did not move (307ms vs 310ms with 40ms of latency per request). The file preview no longer fetches marked and DOMPurify a second time either — the transcript needs them on first paint, so they are in the main bundle. highlight.js stays lazy, being 1MB of it.
0.30.3
Changed
- The web UI loads about twice as fast, and asks for a fifth of the requests. Measured cold (fresh browser profile, no cache, real Chromium) with 40ms of latency on every request — the case that matters, since the daemon is usually reached over a network rather than from localhost. Up to the moment the composer is usable: 56 requests and 113KB became 11 requests and 57KB, the composer was ready in 310ms instead of 470ms, first paint came at 340ms instead of 504ms, and the whole load transferred 114KB instead of 234KB. On a session with 240 messages the transcript was on screen at 636ms instead of 783ms, having parsed 913KB of JSON instead of 1569KB. Three things did it:
- The app is bundled for production. The browser used to fetch it as 45 separate ES modules in a graph six levels deep, and every level of that graph is a round trip. The built copy is now one hash-named bundle plus a chunk per click-gated feature. Served from
src/in development the modules stay exactly as they were — unbundled and debuggable without a build step. - Code behind a click is loaded behind a click. The settings modal and its sixteen panels, the dashboard, the new-session and share modals, and the workspace panel with its three explorers — about 150KB — used to load before the first paint whether or not anyone opened them. They now load on demand, and are prefetched once the page goes idle, so the first click still opens instantly.
- Fonts are woff2 instead of TTF, and immutable. The default face went from 112KB to 37KB, and its URL now carries a content hash, so it is fetched once ever instead of once an hour; all six faces together dropped from 1MB to 348KB.
- The app is bundled for production. The browser used to fetch it as 45 separate ES modules in a graph six levels deep, and every level of that graph is a round trip. The built copy is now one hash-named bundle plus a chunk per click-gated feature. Served from
- A streamed answer costs the browser about a third less work. Every repaint of the block being streamed replaces its HTML, and the browser then re-parses and re-lays-out all of it — a cost that grows with the answer while the render rate did not. Traced on a CPU throttled 6× (a mid-range laptop), one eight-second answer spent 1.1s in layout alone, with or without a transcript above it. The repaint interval now scales with the answer's length: a short one keeps the full ~12fps, a long one settles at ~4fps, where nobody is reading the tail as it lands. Measured over three runs each: 2 683ms of blocked main thread became 1 742ms, 31 long tasks became 21, the 95th-percentile frame gap 83ms became 50ms, and the frame rate went from 41 to 49. Streaming renders also no longer evict the markdown cache they share with the finished transcript — a hundred intermediate versions of one answer used to push everything else out of a 300-entry cache.
- Opening a session fetched and rendered it twice. The event stream refetches the session on connect, to catch anything the daemon did while the tab was away — but that fires on the first connect too, right after the session was just fetched, so every session open downloaded, parsed and re-rendered its whole page a second time: 460KB of JSON for a 240-message session. The first connect after a fetch now skips it.
- A stale name in the asset-version list took the whole page down. The list of modules mixed into
app.js's content hash is hand-maintained; a name whose file no longer exists threw inside the static handler, so every asset 404'd — includingindex.html, with nothing in the log to say why. A missing entry is now just a missing version, andCAST_DEBUG_STATIC=1prints what a swallowed static-file error actually was. - The service worker precached the wrong files. Its shell list named
/app.jsand the vendored preact/htm modules — none of which the bundled page loads — and missed the bundle itself; the build now writes the list from what it actually emitted. A single missing entry also used to fail the whole install (one rejectedaddAll) and leave the page with no service worker at all, silently; entries are added individually now.
0.30.2
Changed
- Lists read as lists. A task list printed its source —
• [ ] сделать— and now draws the box itself,☐and☑, in place of the bullet rather than after it. A nested item keeps the indentation it was written with instead of a fixed two cells per level, so an item under1.(three cells) lines up with its parent's text rather than sitting a cell short of it. And each level gets its own bullet shape,•◦▪— the old•–·made the third level look like a smaller second.
Fixed
- A
---line vanished, and the next table lost its header divider. A horizontal rule matches the same pattern as a table's|---|---|alignment row, and 0.30.1 started dropping those wherever they appeared — so a thematic break disappeared from the answer and the table after it was rendered as if its header belonged to an earlier chunk. The pipe is what makes it a table rule now.
0.30.1
Added
- Code blocks are syntax-highlighted. A snippet in a reply was one flat colour, which is where a terminal gives up on the thing it is best at — strings, comments and names carry the shape of the code, and the eye finds them before it reads the words. highlight.js does the tokenizing (through its emitter, not its HTML), across 25 curated grammars: an unknown or missing language tag leaves the block plain rather than guessing, because a wrong guess colours a snippet misleadingly. A
```diffblock gets its additions and deletions coloured, so a patch in an answer reads like a patch.
Fixed
A code block stopped being highlighted halfway down, and a table after it came out as raw
|rows. The stream cuts an answer into chunks at line boundaries and promotes each separately, so a chunk routinely begins inside a fenced block — with its```tsopener, language tag and all, left behind in an earlier chunk. Worse, a chunk that *began* with the closing```read it as a new opening fence and swallowed everything after it as flat code. The open fence (and its language) is now threaded through the chunks, in the live region and in the committed transcript alike.Tables are drawn as a grid.
| a | b |rows became columns separated by air, and a header marked only by bold — which is nothing at all on a terminal that renders bold as a faint colour shift. Now there is a box:┌─┬─┐, the header ruled off with├─┼─┤, borders dim so the content stays the loudest thing in the block. It costs three cells per column and two rows, which is the difference between a table you read and a table you count columns in.A table streamed into the transcript came out with its alignment row as data, and two different column layouts. Column widths are computed per chunk of the answer, and the stream cuts chunks at line boundaries: a table split across two of them was laid out twice at different widths, and the chunk that began with
|---|---:|had no header above it — so the alignment row itself became the table's header,--- ---:and all. A table under construction now stays in the live region until it ends, so it is always laid out whole; and an alignment row with no header above it is dropped rather than drawn, for a transcript rebuilt from a session where the split already happened.
Changed
- One separator everywhere:
–. Hints used—, content labels used·, and both appeared in the same frame. The picker and palette footers, the composer placeholder, tool rows (read src/ui/ChatLog.tsx – lines all), the MCPserver – toollabel, the subagent summary, the turn footer (provider – model – Ns) and the web UI's equivalents all use the en dash now.
0.30.0
Changed
- A long request wraps as you type it. The composer used to slide a one-row window sideways, so a long draft was a keyhole: no way to see the sentence you were writing. It wraps at the terminal's edge now, on word boundaries, measured in display cells (wide characters and paste chips included) — while staying bounded at three rows, which is what keeps the live region inside the terminal. ↑/↓ move between visual rows, wrapped ones included, and recall a prompt from history only at the very top or bottom of the draft.
- A prompt can have line breaks in it. Nothing was bound to
insertNewline, so a multi-line message could only arrive by pasting: Shift+Enter and Alt+Enter now insert a line break, and on a terminal that reports neither (most of them send the same byte for Enter and Shift+Enter) ending the line with\and pressing Enter does the same. ↑/↓ move between the lines of a draft and fall back to prompt history at its edges. /keyssays how to type a line break, and recall gives back real lines. The new binding was listed as the bare ideditor.insertNewlinewith no hint that a backslash works where Shift+Enter cannot, andCtrl+Lwas missing entirely. Recalling a multi-line prompt from history handed back an opaque[Pasted 3 lines]chip that could only be deleted whole; up to three lines it now comes back as editable text (a bigger block stays a chip — the composer shows three rows).- Tab completes file paths. Outside the
/palette Tab did nothing. It now finishes a path-shaped token (one containing/, or starting with~): the unambiguous part is committed, a directory gets its trailing slash so the next Tab descends, and when several names still match they are listed under the composer. Tab in ordinary prose still does nothing — with no popup to dismiss, a filename the user did not ask for is worse than no completion. - The speaker's label sits on its own row.
you,agentandreasoningused to ride the first line of their text, which cost every row the label's width — 11 cells of an 80-column terminal for a reasoning block — and left consecutive turns starting their prose in different columns until the labels were padded to a shared field. Now the label is a row, and the text under it starts in the same column as everything else: nothing to align, no width given up. The cost is one row per block, which the live-region clamp charges (a block it forgets to charge for is exactly how the region grows taller than the terminal). - Typing while the agent works steers it. A plain message sent mid-turn is injected into the running turn — no
/steer, no/s. That is what the message means when it is written mid-run, and it is what the web UI already did (the daemon steers anything sent into a running turn), so the two surfaces finally agree; the TUI used to refuse the line and tell you to go read about/queueand/steer./steerstays for being explicit and for scripts. An image can't be injected mid-turn — that says so instead of dropping the picture silently. The composer's hint follows:type to steer the running turn — esc esc to stop. ·is gone from the hint lines. Composer placeholder, the/palette footer and every picker footer separate their hints with—now.- Every row under the transcript is one row again, on any terminal width. The composer's placeholder, and the status bar, still wrapped: at 24 columns the bar took two rows and the placeholder three, growing the live region the rest of this work exists to keep fixed; at 40 the elapsed counter painted over the tail of the model name, so
test-modelreadtest-mode0.4s. Both truncate now, with the status bar's right-hand group keeping its width. The composer's hint is also shorter and less shouty —ask cast to do anything, andesc esc to stop · /queue · /steerwhile a turn runs; the commands it used to spell out are in the/palette. The divider above the composer spans the full width instead of stopping one cell short.
0.29.0
Changed
- Unregistered daemons piled up. Every client — the TUI,
cast run, ACP,cast server status/stop— finds the daemon through~/.cast/server.json, so a daemon that is no longer recorded there cannot be reached by anything. Such an instance is created on purpose (a process that has already bound its port keeps serving rather than dying mid-request) and nothing ever ended one, so they accumulated: measured on one machine, four daemons at 160–250MB each, three of them unreachable. An unrecorded daemon now retires itself — but never because its clients went away, which is the daemon's whole purpose: the user closes the terminal, goes to bed, and the agent keeps working. It retires only when nothing at all is happening — no turn running, no background bash task alive, no subagent or checkpoint writer in flight, nobody subscribed — and has been that quiet for ten unbroken minutes, measured from a watermark that any event moves, not from a sampled flag (a turn that starts and finishes between two 30-second polls is invisible to sampling — measured — and must still count). Nor within the first minute after startup, because the record is written after the bind and another process may be mid-write. A--foregrounddaemon is never retired: the user is looking at it. Verified against the built binary: with the default 30s/60s timings an unregistered daemon exits after 60s; one with a turn in flight waits for the turn; one with asleep 120background task stays; one with an open event stream stays until the client disconnects; and one that still holds its record is untouched. - The scrollback no longer disappears while you type a long prompt, queue a message mid-turn, or print a long listing. Only the streaming answer was height-bounded; the composer draft, the
[Queued: …]receipts and the notice line wrapped to as many rows as they wanted, and once the live region is taller than the terminal Ink clears the screen and the scrollback on every frame — 13 full clears for one 600-character draft, 210 (1.8MB of output) for two long queued messages during a stream, one for a 30-hook/hookslisting. The composer now windows a long draft horizontally like readline, with\u2039/\u203amarkers at the clipped edges; queue/steer receipts are one truncated row each, at most three plus a count; and a notice taller than three rows goes to the transcript, where it can be scrolled back to, instead of the live line.npm run e2e:tuicovers all three. - Exiting prints the command to come back.
cast --resume=<id>on the way out, so the session id is not lost with the frame the exit clears (it is nowhere else on screen). Nothing is printed for a session with no turns. - The ASCII wordmark is gone from the TUI. Seven rows of block art cost a third of a 24-row terminal before the first message, had to be reprinted on every terminal resync (and de-duplicated when that reprint left the old copy in the scrollback — a window drag stacked a wall of them), and said nothing the status bar does not. Startup prints one gradient line,
cast v0.29.0. The web UI keeps the logo, where a logo makes sense. - The TUI renders the answer instead of printing its markdown. A reply arrived looking like source:
## heading,**bold**, backticks, ``` fences,|---|table rules,>quotes — all visible as characters. Now headings, emphasis and inline code are styled, fenced blocks are coloured as code, lists get a bullet and a hanging indent so a wrapped item lines up under its own text, quotes get a gutter, links show their text and target, and tables are laid out in aligned columns that shrink to the terminal's width. Every row carries a marker in one gutter column, so the transcript has a single left edge and a wrapped paragraph reads as part of the reply it belongs to instead of starting at column 0: a coloured▌for a turn, a dashed┆for reasoning, a grey│for the scaffolding around them — tool calls, errors, retries, the activity spinner — andⓘfor a harness notice, which replaces the[system]prefix the text used to carry. The speaker's label rides the first line (▌ you проверь…,▌ agent Готово) rather than taking a row of its own, with continuations indented to the same column; two rows of chrome per turn is a lot on a 24-row terminal. Two themes (nord, solarized) putmutedwithin 1.7–2.8:1 of their background, where a one-cell bar disappears, soThemeColorsgrew an optionalrailcolour for them —border, despite the name, is 1.3–2.5:1 everywhere and unusable as a rail. The truncation marker is drawn inside the label's field rather than appended to it: appending made every row of a clamped block two cells wider than the width it was rendered for, Ink wrapped them all, and the live region doubled in height — 63 full-screen clears in a single streaming answer, the exact failure the clamp exists to prevent. A test now renders the frame and compares its height against what the clamp charged. Tool rows are quiet scaffolding rather than a third loud element:[bash] [ok] command="git status --short"is now a dimmed grey│ bash git status --shortwhose bar sits in the gutter column itself, level with a turn's▌: the transcript keeps one left edge, and the difference between a reply and its scaffolding is carried by weight and grey rather than by indentation. A thicker┃marks a call still running, a difference in weight rather than colour; colour is spent only on a failure (✗), where the contrast is worth something. - The live-region clamp counts rows it rendered, not cells it estimated. The renderer wraps to the width it is given and returns lines, so the number of rows a block will occupy is exact by construction — and the same lines are handed to the view, so a frame renders once. The cell arithmetic this replaces is where a CJK answer took twice the rows it was allowed. Verified with
npm run e2e:tui: 0 full-screen clears while a long Japanese answer streams.
0.28.0
Added
- ↑/↓ recall previous prompts, as in any other terminal input. They were wired to the text buffer's cursor-up/cursor-down, and the composer's buffer is one line by construction — so ↑ did nothing at all and ↓ jumped the cursor to the end of the line. Now ↑ walks back through the prompts submitted in this session (seeded from a resumed session's transcript), ↓ walks forward, and stepping past the newest entry restores the draft that was being typed. A recalled multi-line prompt comes back as a paste chip, so re-sending it reproduces the original text exactly. While the command palette is open the same keys still move its selection.
Fixed
- The context indicator's two halves disagreed.
ctx <used>/<total> (<pct>%)computed the percentage against the input budget — the window minus the reply reserve, which is what compaction measures against — while printing the raw context window as the denominator. A 128k model with the default 32k reserve therefore renderedctx 94.7k/128k (99%), where 94.7 of 128 is 74%, and a 32k model renderedctx 94.7k/32.8k (578%). The denominator is the budget now, so the fraction and the percentage say the same thing and the percentage still means "how close am I to a compaction". - A multi-line tool summary took more rows in the live region than it was charged. The clamp counts a tool call as one status row, and Ink's
wrap="truncate"truncates the text but does not remove its newlines — a value that already fits the terminal width comes back unchanged. So ataskwhose assignment reads "Do X / Then Y / Report back" rendered three rows against a one-row charge, and any tool call whose streaming arguments arrive pretty-printed did the same, on every call. Enough of those and the live region is taller than the viewport, which is the state Ink handles by clearing the terminal and replaying all of its static output — on every frame. The compact row is collapsed to one physical line; committed history still shows the assignment as written. - One backspace deleted part of an emoji. The composer's cursor moved and deleted by code point, so a family emoji (
👨👩👧👦, four people and three joiners) lost one member per press and left a dangling joiner behind — the glyph fell apart into separate emoji on screen and took seven presses to remove. A skin-tone modifier came off its emoji, a flag lost half its pair, and an accent written as a combining mark came off its letter. Cursor movement, backspace and delete-forward now step over whole grapheme clusters, and the cursor cell renders the whole cluster instead of its first code point. The boundary lookup is windowed around the cursor because segmenting a long pasted draft costs 49ms at 100KB — on every keystroke — against 0.13ms for the window. - A long line of CJK text overran the live region. The clamp that keeps the streaming preview inside the viewport hard-cuts a single over-long line, and it sliced that many characters against a budget counted in cells — the comment beside it even claimed this erred short. It erred long by exactly the character's width: on an 80-column, 24-row terminal a 20,000-character CJK line kept 1,280 characters, which is 2,560 cells and 32 rows against a 16-row budget. That is the one thing the clamp exists to prevent: a live region taller than the viewport makes Ink clear the terminal and replay every static row it has ever printed, once per frame. Measured end-to-end in a pseudo-terminal (
npm run e2e:tui) on a 30-row window while a Japanese answer streamed: 51 full-screen clears with scrollback wipes and 209KB of terminal traffic before the fix, 0 clears and 24KB after. The cut is now measured in cells, and errs short on a cluster it splits. - A failing callback under
suspendAndRunran twice. The fallback path there is for a suspend hook that refuses (Ink throws when the terminal is already suspended), and a callback that simply failed was indistinguishable from that: its error was swallowed and the work was retried — the second time with the terminal no longer suspended, so whatever it printed landed on top of Ink's frame. It now tells the two apart and propagates the callback's own error. The module header also described pipingprocess.stdininto a child process, which it never did:bashruns with stdin at EOF on purpose, so a command that waits for input exits instead of hanging the session. - A finished background task showed up as the user's own message, raw XML included. With a daemon running the daemon injects the completion notice, and the TUI appended that message to the transcript verbatim — so the notice arrived attributed to the person, with a bare
</system-reminder>visible on its own line, instead of as a[system]row. Every other surface already split those blocks out; this one path (the daemon'suser_messageevent) did not, and it is the path every daemon-injected reminder takes: background tasks, the post-compaction state block, attached-file lists. Every path now shares one function — including the follow-up injection, which is how a background task that finishes during a turn reaches the transcript: that one appended the message verbatim too, so the notice arrived asyou <system-reminder>…with the raw tag on screen. /undosaid there was nothing to undo, right after a change. With a daemon running — the default — the daemon owns the turn and appends the turn's checkpoint itself, while the TUI's in-memorysession.checkpointsis only ever filled when a session is loaded. So/undoimmediately after an edit answered "[No checkpoint available to undo]", and the command only started working after a restart, which is the one moment nobody needs it. It now reads the persisted list — the same table both paths write to. Verified end-to-end in a pseudo-terminal: the agent writes a file,/undonames it in the confirmation and removes it, with the repository's tracked files untouched./helpwhile the agent was working answered "use /queue, /steer, or /abort". Only five commands could be sent mid-turn, so every local, read-only one —/help,/keys,/current,/context,/rulesand the/themeswitch — was refused with advice meant for a prompt, and the line stayed in the composer as though Enter had been swallowed./reasoning-display, a display toggle of exactly the same kind, was already allowed, so the gate was inconsistent as well as unhelpful. Those six now run while a turn streams; anything that changes what the running turn does (/model,/compact,/undo,/clear) still waits, with the same explanation as before.- Terminal width was measured with a hand-rolled table that was wrong in both directions. The live region decides how many rows a streaming block needs from these numbers, while Ink renders it using
string-width, so a disagreement either drops text that would have fitted or lets the region overrun the viewport — the failure the measurement exists to prevent. Checked againststring-widthon 33 strings, the old table disagreed on 8: an emoji built from a ZWJ sequence measured wildly wide (👨👩👧👦as 11 cells instead of 2,👨💻as 5,👍🏽as 4), a combining accent added a phantom cell (écoleas 6 instead of 5), and🀄,🈁,⌚measured 1 cell instead of 2. Width now comes from the Unicode East Asian Width property with joiners, variation selectors, skin tones and combining marks folded into the glyph they attach to; all 33 agree, and it stays a per-code-point loop becausestring-widthitself costs 46× more (647ms against 14ms on one 240KB line, well past a frame's 16ms). - Pasting an image said "no image in clipboard" when the helper binary was missing.
readClipboardImagecomposes exactly the message the user needs —pngpaste not found — install: brew install pngpaste— and two things threw it away. The TUI's paste handler collapsed every failure tonull, which the composer renders as "No image in clipboard — copy a screenshot or image file first"; and the check for a missing binary tested forENOENTwhile the command ran through a shell, which reports a missing binary as exit 127 and never as ENOENT. pngpaste is not part of macOS, so a Mac user with a screenshot on the clipboard was told the clipboard was empty, every time. The commands no longer go through a shell (the argv was fixed anyway), the real reason reaches the notice, and a clipboard that genuinely holds text instead of an image is still reported as "no image" rather than as a tool failure. A saved clipboard image is also written 0600 — /tmp is shared, and a pasted screenshot is whatever was on the screen. - A command's output could forge cast's own notices. Completion notices for background tasks are
<system-reminder>blocks — the channel the harness uses to talk to the model, which the TUI and web UI render as[system] …rows and the ACP adapter drops from a replay. The task's command and output were embedded in one without escaping, so output containing</system-reminder><system-reminder>…closed cast's envelope and opened its own: one notice became five reminder blocks, one of them authored entirely by the command. The model reads that block as an instruction from the harness rather than as data a command printed, and the user is shown it as a genuine[system]notice (or, in an editor over ACP, not shown it at all). Ordinary output can do this by accident — a build log echoing a prompt file — and a hostile repository can do it on purpose. The same escaping now applies to the editor buffers the ACP adapter injects as context and to the checkpoint-validation report, the other two places where untrusted text sits inside that envelope.
0.27.0
Changed
disposeSessionWorktreeis gone. Nothing called it, and it removed a worktree withgit worktree remove --forceplusgit branch -D— the two guards/worktree removewas fixed to stop bypassing. Dead code that discards uncommitted work and unmerged commits is one call site away from doing it again.
Fixed
- A share link handed over every command's output. The public
/api/shared/:tokenview dropped the persona's system prompt and the web UI hides tool cards in that view, but the JSON behind it is unauthenticated and carried each tool call's full arguments and full result — so a "read-only conversation link" sent to a colleague was also a transcript of every file the agent read and everything every command printed,curlaway. The live relay had already been fixed to hide exactly this; the saved view it was fixed to match never actually hid it. Both now redact the same way: the visitor sees thatbashran and whether it succeeded, never the payload. The<system-reminder>notices a turn carries (project memory, attached documents) are dropped from the shared view too. /undocorrupted the files it restored, in a project without git. The shadow-checkpoint path read a file withreadFileSync(path, "utf8")and wrote it back the same way, so every byte that is not valid UTF-8 came back asU+FFFD— a 12-byte PNG was "restored" as 22 bytes of replacement characters, reported as success. Snapshots are taken and restored as raw bytes now, and a file above 10MB is recorded as un-restorable instead of being copied into the session store, with the restore saying which files it left untouched./undo's confirmation listed files it was not going to delete. It comparesgit clean -ndoutput against the checkpoint's tree, but the first is relative to the session's directory (with a./prefix) and the second was listed relative to it too, or scoped away entirely — so from a subdirectory nothing matched and the prompt claimed the restore would delete files it in fact puts straight back, including ones the user wrote themselves. It also passed git's own "Would refuse to remove current working directory" notice off as a file. Both sides are compared as repository-relative paths now, and onlyWould remove …lines are read as paths.editwithreplaceAllexpanded$&in the replacement.String.replaceAlltreats$&,$`,$'and$1in the replacement as substitution patterns, so anewStringcontaining them — sed, a regex, jQuery, shell — was written back with the matched text spliced into it.edit's single-occurrence path concatenates and always did the right thing, so one tool wrote two different results depending onreplaceAll:s/x/$&y/landed ass/x/s/a/b/y/. Same fix for the skill-template placeholders (${CAST_SKILL_DIR},$ARGUMENTS, named arguments) and the UI factory's{{var}}substitution, where the replacement is a path or whatever the user typed.- The SSH control socket lived in a directory anyone could own. cast multiplexes SSH connections through
/tmp/cast-ssh-ctl, and a master socket there stays usable for an hour (ControlPersist=3600): reaching it is enough to run commands on the remote host as the user, with no key and no password. The directory was created withmkdirSync(…, { recursive: true }), which silently accepts a path that already exists — a directory or a symlink another local user planted in the shared/tmpfirst — and thechmod 700that followed was a swallowed best-effort. Verified: with the path symlinked elsewhere, the socket landed at the link's target. The path is now per-uid and checked after creation (a real directory, owned by this user, mode 700) or refused with a message naming the problem. Exiting cast also no longer deletes the directory: that removed the live sockets of every other cast process on the machine, and an orphaned master unlinks its own socket when it times out. - Project memory was silently amputated at 40,000 characters. One read helper served both jobs — pasting a memory file into the prompt, where a cap is right, and reading it in order to write it back, where it is data loss.
MEMORY.mdis the canonical store: dream merges new bullets into it, a rejected checkpoint is rolled back from it, the database is reconciled against it. So the first merge after the file passed 40,000 characters rewrote it as its own first 40,000, deleting every bullet beyond them — the file whose own code comment promises that hand edits and curated markdown survive intact. A rolled-back checkpoint andnotes.mdappends lost the same way. Reads that write back now see the whole file; only prompt assembly caps, and it marks the cut so the model does not read a truncation as "not in memory". - Upgrading from the web UI killed the daemon and did not bring it back.
POST /api/system/upgraderuns the upgrade inside the daemon, and the restart step signalled the recorded pid — which is that same process. The shutdown handler then closed every session and exited within seconds, so the step that was supposed to start the replacement never ran: a freshly installed cast, no daemon, and a web UI that simply went dead. The self case now hands the restart to a detached waiter that starts the new daemon once the old pid is gone, and shuts down normally so sessions still drain (verified live: old pid gone, new daemon listening on the same port). A pinned version is also validated asx.y.zbefore it becomesCAST_VERSIONfor the installer. cast server startfailed on a machine where~/.castdid not exist yet — a fresh install or a container image, where the daemon is the first cast command run. It opened its log file before anything created the directory and exited with an ENOENT, advising the user to check a directory that was never there.- The web UI escaped
&,<and>but not quotes. Rendered markdown is inserted as HTML and link targets are interpolated intohref="…", so a URL containing a double quote closed the attribute and added an event handler that then ran in the daemon's own origin — session cookie and full API included. Markdown reaches the renderer from model output, tool results and file contents, none of which is under the user's control. Quotes are now escaped, and the renderer moved into its own module so this is testable.
0.26.1
Fixed
- One definition of "the project" for rules, memory and history search. Each subsystem answered the question differently, and a session started in a subdirectory paid for it:
AGENTS.mdwas inherited from every ancestor, but.cast/ruleswas only read from<cwd>downwards, project memory was keyed on a hash of the exactcwd, and a project-scoped history search matcheds.cwdexactly. In a monorepocd apps/web && casttherefore lost the repository's rules —/ruleslooked empty, as though none had been written — started from an emptyMEMORY.md, the file whose own template says it is "shared by all sessions", and could not find any session run from the root. The project root is now the nearest ancestor with a.git, else the topmost with a.cast/, else the directory itself, and rules, memory, history scope and glob-matched context paths all use it. A nested checkout stays its own project (its own.gitsays so), a subdirectory's.cast/rulesstays what it is documented to be — rules scoped to that subtree — and the home directory is never a root, so~/.castremains global configuration and a dotfiles repository in$HOMEcannot make everything under it one project./reloadre-reads the root.
0.26.0
Added
- Automatic compaction is configurable.
contextWindow,maxResponseTokens,compactionThreshold,maxToolOutputLinesandmaxToolOutputBytesare settings rather than constants nothing could reach —/currentused to show a budget the user had no way to change. Each clamps to a sane range and falls back to what the constant was, so an absent or malformed value behaves exactly as before. An explicitcontextWindownow also beats the model catalog, for an endpoint the catalog gets wrong. - Waiting out an exhausted quota. A quota error is normally terminal — credit does not come back on its own — but when the key's limit is a window (daily tokens, hourly requests) it does reopen, and an unattended run should sit through that rather than dying at 3am on a limit that clears at 4.
retryQuotaWaitSeconds(default0, off) is both the switch and the whole budget: with it set, the quota is re-tested on a 30s→5min backoff, or exactly when the provider'sRetry-Aftersays, until the budget is spent. Esc cancels the wait.retryMaxWaitSeconds(default 3600, previously a hard-coded 10 minutes) caps a single header-supplied wait; its floor is the old constant, so a broken settings file can only make cast more patient, never less. - Dangerous-command confirmations under the daemon. The prompt existed in the TUI and was skipped entirely when the same command ran through the server, which is what the web UI,
cast runand ACP all use. The confirmation now travels to whichever surface is attached, with a five-minute timeout that refuses rather than assumes. - A session's own project MCP servers. Under the daemon only the global
~/.cast/mcp.jsonwas connected, so a project's.cast/mcp.jsonwas silently ignored — the servers a repository ships for itself never appeared. Project servers are now started per directory and released when the last session in that directory goes away, on every path that drops one (close, idle eviction, permanent delete, throwaway session), rather than leaking processes.
Changed
- The compaction tail scales with the window. The 10k–20k envelope was written for a 128k model, where 20k is a sixth of the budget. On a 1M model the same constant made compaction a near-total reset: it fires at 726k tokens and left about 28k behind, discarding 96% of the context in one step. The ceiling is now 20% of the input budget, never below the old 20k and never above 100k — deliberately a maximum over the old constant, so every window up to 128k is untouched. Measured on one 1.26M-token history: 64k and 128k keep 21,106 tokens exactly as before, 200k keeps 31,645, and 1M keeps 94,882.
- Rules use Cursor's format. cast reads
.cursor/rulesas well as.cast/rules,.mdcas well as.md, and subfolders inside a rules directory — a project that already has Cursor rules needs no second copy of them. Globs follow minimatch strictly (*and?never cross a/), which meansglobs: *.tsmatches only the repository root, as it does in Cursor; write**/*.tsto cover a file type project-wide. - The TUI's command dispatch is a registry.
handleInputwas a 2,228-line if-chain; it is now 47 lines over a table of routes that declares, per command, whether it may be submitted mid-run. Every routed command is exercised by a test,/quitincluded — it was the one regression the refactor introduced and the tests caught.
Fixed
- Automatic compaction never fired on the history that needs it most: one request followed by a few enormous tool results. Three defects stacked up, each turning compaction into a silent no-op — it ran every round, compacted nothing, emitted no event, and the context grew until the provider rejected it. The tail's five-prose-message minimum cannot be met by a handful of huge tool results and nothing capped the search, so the tail grew backwards until it swallowed the whole transcript; the fixed 10k–20k tail envelope ignored the model's own window; and the cut point could only be a user turn, of which a single agent run has exactly one, at index 0. An assistant row is just as safe a boundary — tool results always follow the assistant message that declared their calls — and is now the fallback. A compaction that finds nothing to do also says so instead of staying silent.
- A 32k model reserved 32k for its reply.
maxResponseTokensdefaults to 32,000, which is right for a 128k model and nonsense for a 32k one: the input budget is the window minus the reserve, so a 32,768-window model got a 768-token budget and compacted on its first tool call — a live run reading a 16-byte file compacted at 8,080 tokens of context, and would have again every round. Below 32k the budget went negative, which made the compaction check unconditionally true and collapsed the tail to a single message. 676 models in the catalog have a window of 32k or less. The reserve is now capped at half the window and the budget derived in one place; a 128k model is untouched. - Compaction summaries duplicated their file tags and invented facts. The
<read-files>block is appended deterministically from thetool_calls, but the previous summary was handed to the model with the tags still on and the update prompt says "PRESERVE all existing information" — so it copied them in and cast appended its own underneath, one more copy per round, with the model's transcription then displacing the real extraction. Separately the summarizer saw only the first 500 characters of each tool result, so it recorded "no MARKER value" for a file whose marker was on the last line — a false fact that outlived the messages it came from, and that one round explained with aGrepcall that never happened. It now sees head and tail with the cut marked, and both compaction prompts say not to read a cut as absence or invent how something was established. - An oversized compaction summary grew the context. Nothing bounded the summarizer's answer, so a model that replied with a wall of text made compaction increase the context (measured: 242k characters in, 472k out) — the next turn then sat closer to the ceiling and paid for another summarization that could do the same. Clamped at 8k tokens with a note saying it was truncated.
- A provider's
Retry-Afterwas clamped to the guessed-backoff ceiling. It is not a guess — it is the provider saying when its window reopens — but it went through the same 30-second cap, so a 429 saying "retry in 150s" was retried at 30s, refused again, and the turn died on the 120-second retry deadline having never once waited as long as it was told to. The deadline now bounds only cast's own guessing; an explicit instruction moves it out. Backoff also gets 25% jitter, so parallel subagents that hit one limit stop retrying in lockstep. cast rundropped theretryevent entirely. A retry can be a long wait — minutes to hours with a quota wait configured — and it printed nothing on either stream, which reads as a hang.- The daemon answered from the wrong directory. A session's rules, skills,
AGENTS.md, ssh hosts, project trust and MCP servers were all resolved from the daemon's own working directory rather than the session's, so two sessions in different projects saw one project's configuration. Each is now resolved and cached per session directory, and cleared by/reload. - Rules: a
globs: *.tsxline threw a YAML alias error out of every caller instead of failing as one bad rule; nested rules matched their globs against the wrong path; a rule body of any size was injected whole (now truncated at 64KB with a note); and a rule file deleted mid-session kept applying. /undodeleted files without warning. Restoring a checkpoint removes files created during the turn, which the confirmation never mentioned. It now names them and asks.- Share links leaked through the live relay what the saved share view hides.
- bash: cast failed to start where node-pty's native module cannot load — a distro Node links
libnode.sowhile the official tarball is static, so a.nodebuilt against one is not portable to the other. The module is loaded lazily and background tasks fall back to a plain pipe rather than taking the whole tool down with them.
0.25.0
Changed
- Debugging discipline in the shared prompts. Three general rules, each measured on the behavior bench rather than guessed at. (1) When a task starts from something failing, running it to see the failure is the default first action rather than opening the source to reason about it — with one exception, added after both MiniMax-M3 and mimo-v2.5 declined the rule on a single-line operator inversion and were right to: skip the reproduction when what you read leaves exactly one nameable cause. Anything vaguer ("probably the cache", "looks like a race") still gets run, and verifying after the change is unconditional. (2) Inspect the tree with the tools (
ls,glob,grep,read) instead of shelling out tols -la/find/cat, and never probe for a path's existence before calling the tool that wants it — the tool's own error is more informative than the probe. Agrephit is still a pointer, not an answer: the file has to be read before saying what the code does. (3) In plan mode, ask only when the answer changes the plan — if rival answers produce the same Steps and differ in a detail (who does it, what it is called, when), write the step and park the detail in Assumptions with a pre-decided fallback. A question that stops the turn without changing what gets written spends the user's turn for nothing. Verified by A/B with alternating runs on identical code: 30/27/35 before the prompt changes against 35/33/36 after, a win in all three pairs, and the same average on a second model (mimo-v2.5, 35.3) as on MiniMax-M3.
Fixed
- bash: the tool never said that each call is a fresh shell. Every invocation starts in the session's directory, so a
cddoes not carry into the next call — undocumented, and a trace caught the model losing its directory exactly that way:cd <dir> && node check.jsworked, the follow-upnode check.jsfailed withCannot find module .../check.js. The description now states it. - bash: a background task whose command exited non-zero was reported as the shell having failed to start —
Failed to start bash ("bash"): ls: cannot access '…': No such file or directory. The spawn-failure test matched "no such file or directory" anywhere in the output, which is what any command says about a path it cannot find, so a healthy shell running a failinglswas described as never having started and its real output was buried behind the wrong explanation. Detection now requires node-pty'sexecvp(3) failedmarker or the shell naming itself at the head of a line. - todo_write: the link between a todo and its plan step was lost whenever the model retyped the item.
planStepis carried forward by matching the previous item'scontentexactly, but flipping one status means resending the whole list, and models routinely shorten an absolute path in it (…/note.txtbecomesnote.txt). The link — the open-work gate's only handle on that step — silently went with it, so approved-plan work read as unlinked. A second matching pass compares with directories stripped off path-like tokens. - write: a file ending in a newline was reported with one line too many.
"READY\n"came back asOverwrote … (2 lines)because the trailing empty split counted as a line, telling the model it had written a blank line it never wrote — an invitation to "clean up" a correct file. - write / edit: an exhausted disk quota surfaced as
Unknown system error -122: Unknown system error -122, write. libuv has no name for EDQUOT, so it arrives as codeUNKNOWNwith errno -122 and slipped past every case in the error description. Hit for real on a filled tmpfs, where it also broke writes the agent had every reason to expect to work. It is now named as a quota exhaustion, with what to do about it.
0.24.0
Removed
- Plugin marketplaces. The whole subsystem is gone:
/pluginand its subcommands (install, uninstall, enable, disable, marketplace add/list/remove/update), the Plugins and Marketplace tabs in the web Settings modal, theenabledPluginssetting, the/api/plugin-contentendpoint, plugin-contributed skills and hooks, and the${CLAUDE_PLUGIN_ROOT}/CAST_PLUGIN_*substitutions that existed only for them — about 2,700 lines. Installing a plugin required a registered marketplace, and registering one cloned an entire catalog repository to disk (316MB across the three defaults on one real installation) to browse packages that were never installed. Skills, which is what the catalogs were used for in practice, are better served by installing them individually:~/.cast/skills, a project's.cast/skills, or the universal.agents/skillspath thatnpx skills addwrites to and that cast already discovers. Hooks and MCP servers stay configurable throughhooks.jsonandmcp.jsonas before. An existing~/.cast/plugins/directory is now inert and can be deleted.
Added
- skills.sh in the TUI.
/skills-sh search,list-available,installanduninstallwere reachable only from the web UI; the TUI had no way to install a skill from the universal index at all. Both surfaces now run the same code — argument normalization and thenpx skillscall moved intocore/skills-sh.tsinstead of living inside the web bridge. A pastednpx skills add …line and ahttps://github.com/owner/repoURL are both accepted, and an-a <agent>flag is dropped because that form installs only into one agent's directory, which cast never scans — the skill would silently never appear.
Fixed
- Skills: cast now honours the Claude Code skill fields that change behaviour, not just the six-field Agent Skills spec.
user-invocable: falsekeeps a skill out of the slash menu while leaving it loadable by the model;argumentsmaps named$nameplaceholders onto positional arguments andargument-hintis retained for autocomplete;disallowed-toolsremoves the named tools from the model's pool for the rest of the turn and clears on the next user message;pathslimits a skill to turns where a matching file is in context;${CLAUDE_PROJECT_DIR}and${CLAUDE_PLUGIN_ROOT}are substituted. Inline!`command`blocks — which 194 of 5,639 installed skills use, and which cast previously handed to the model as literal text so a body reading "Node: !node --version" looked like a completed check — now run for skills the user installed themselves, bounded at 10 commands, 10s and 2,000 characters each. They run for every skill regardless of source — refusing marketplace skills wholesale would have blocked the environment probes that are nearly all these blocks do, while a skill can already tell the model to run anything in prose. What a skill body must not be is a way around the checks a plainbashcall faces, so each command goes through the same two gates: plan mode's read-only rule, and the dangerous-pattern confirmation (refused outright when no confirmation callback is available).allowed-toolsstays parsed but unenforced for the same reason — a restriction may remove tools, never grant them. A skill'shooks:block is registered too, inhooks.json's shape: without it a skill whose whole point is "run the formatter after every edit" was inert. They join the run's hooks for the rest of it, andonce: truedrops a hook after it fires without blocking. - Skills — Agent Skills spec conformance: cast rejected skills that the published spec calls valid, so packages from anthropics/skills or
npx skills addwere dropped at load with a diagnostic nobody reads.nameis optional per spec and defaults to the directory name (it was required);descriptionis only recommended and falls back to the body's first paragraph (it was required); anamethat differs from its directory is legal, sincenameis a display name (it was rejected);allowed-toolsaccepts a YAML list as well as a string (only the string form parsed); and boolean fields acceptyes/on/1as well astrue— so a skill markeddisable-model-invocation: yes, its author saying "only the user may run this", had stayed model-invocable. The skill listing now also truncatesdescription+when_to_useat the spec's 1,536 characters, which previously only cappeddescription. - Skills — built-ins:
arxivdeclaredversionandplatforms, fields in no spec; packaging or uploading a skill with an unknown key fails outright rather than ignoring it. They moved tometadataandcompatibility.systematic-debuggingtold the model to usesuperpowers:test-driven-developmentandsuperpowers:verification-before-completion— another ecosystem's names for skills cast ships astddandverification-before-completion— andtddpointed at acode-reviewskill cast doesn't have. A test now fails if any built-in skill names a skill that isn't shipped, or declares a field outside the spec. - Skills: a skill invoked without arguments handed the model its placeholders verbatim.
argsis optional on theskilltool, and the substitution returned early when it was absent, so a body written with$ARGUMENTSreached the model as the literal text$ARGUMENTS— which reads as an instruction to substitute something that already happened. Placeholders are now always resolved, to an empty string when there is nothing to put there; the same applies to${CAST_SESSION_ID}outside a session. - Rules discovery: the walk that finds nested
.cast/rulesdirectories was depth-bounded but unbounded in width, and it runs at startup and on every subagent spawn. A normal project costs nothing (cast's own tree is 68 directories, 13ms), but running cast straight from a home directory walked 9,392 of them — 333ms warm, 2s on a cold filesystem cache, paid again per subagent. The pass is now breadth-first with a directory budget, so it spends its effort on the levels nearest the root where rules actually live: the same home directory now takes 101ms. - bash confirmations:
npm publish --dry-runasked for confirmation as though it were publishing. It is the command a release checklist tells you to run before the real one, so prompting on it trains the habit of confirming without reading. - Plan mode: the read-only bash gate could be bypassed outright. An environment prefix was dropped before any check ran — the parser reports
VAR=value cmdas its own node and the gate discarded those — soLD_PRELOAD=/tmp/evil.so cat file,GIT_EXTERNAL_DIFF=/tmp/evil git diffandPAGER='sh -c …' git logwere all classified read-only and then executed arbitrary code (verified against the gate itself). Separately, several git options run a command rather than describing what to show:--ext-diffand--textconvinvoke the driver named bydiff.<name>.command/.textconv, which a repository's own.git/configor.gitattributescan define — cloning a repository was enough — while-Oruns its argument directly and--exec-pathredirects where git looks for its own subcommand binaries. All are refused now; locale and timezone prefixes (LC_ALL=C sort) and the safe--no-ext-diff/--no-textconvforms still pass. Fifteen tests pin the gate, eleven of which fail against the old code. - ACP: cast's own
<system-reminder>blocks were replayed to the editor as if the user had typed them. They ride onrole: "user"messages because the wire format has no better role — interrupt notices, the post-compaction state block, background-task completions — andsession/loadsent them verbatim, so an editor showed raw XML attributed to the person using it (one real store holds 158 such messages). The web UI and TUI both stripped them already; the ACP replay now does too, and a message that was nothing but reminders is skipped entirely. The stripping itself moved into one shared helper, since the bridge, the TUI and the web client each carried their own copy of it. - Open-work gate: steering mid-turn didn't give the gate its budget back. The gate that nudges the model when it stops with approved-plan work still open is capped "per user prompt", and the follow-up path reset that counter — but a steer did not, so a message sent after the gate had spent its two nudges left the model free to stop with the work still open, exactly when the user had just asked for more.
- write / edit: a filesystem error came back as an unexplained internal failure, and sometimes as a misleading one. Writing to a path that is a directory reported
write failed unexpectedly: EISDIR: illegal operation on a directory, read— the read being the load of the previous content for the diff, so the message pointed at the wrong operation — andediton a file the agent may read but not write threw its EACCES straight out of the tool. Both now name the actual obstacle (a directory, permissions, a read-only filesystem, no space left, a symlink loop) and the file it applies to. - todo_write: the list had no bounds, even though it is re-rendered into the system prompt on every build-mode turn and persisted with the session — so one oversized write is a tax on every later request and survives a restart. A 500-item list and a single 100,000-character item were both accepted. At most 100 todos and 500 characters per item now, with the limit named in the error so the model can split the work. Two todos with the same
contentare also refused: content is a todo's only identity (plan-step links and the TaskCreated/TaskCompleted hook diffs are keyed by it), so a duplicate silently inherited the other's plan step. - skill: a skill whose file disappeared after discovery (uninstalled, worktree switched, repo moved) reported
skill failed unexpectedly: ENOENT: no such file or directory…— accurate, and useless to the model. It now says the skill's file is unreadable, names the path, and suggests continuing without it. - memory / session_history: a bad argument came back looking like an answer. Both tools took their
limitasNumber(args.limit) || <default>, solimit: -3reached SQL as a negative limit and returned a single row — reported as "Found 1 match", indistinguishable from there genuinely being one —limit: 0silently meant the default rather than nothing, and a fractional limit surfaced SQLite's own "datatype mismatch" as an unexplained tool failure. An unrecognizedscopewas equally quiet:memoryfell back to the current project andsession_historyto the project scope, so a model that mistypedsessionsassessionwas answered from a scope it never chose. All of these are now refused with the valid values named, matching what every other tool'slimitalready did. - Worktree:
/worktree removedestroyed uncommitted work and unmerged commits without asking. It passedgit worktree remove --forceandgit branch -Dunconditionally, bypassing both of git's own guards against exactly that — so removing a worktree you had forgotten held unsaved edits lost them, and a branch whose commits existed nowhere else went with it. Removal now asks git nicely: a tree with modified or untracked files is refused with the path and the--forcecommand spelled out, and a branch git won't delete is kept with an explanation.--force(or-f) on/worktree removeopts back into discarding, in both the TUI and the web UI. - Tool calls: a tool invoked with no arguments could never run on some providers. Several OpenAI-compatible providers send
arguments: ""(or omit the field, which accumulates to the same) for a call that takes no parameters —lsandplan_doneboth allow that — andJSON.parserejected it, so the model got back "Tool call arguments were truncated or malformed (invalid JSON). Retry the tool call" and could only answer by retrying the identical call until the doom-loop guard stopped it. Empty arguments now mean an empty argument object, which is what they are; genuinely truncated JSON is still refused as before. The same empty string also made the Hermes-XML recovery path treat every such call as mis-serialized. - task: removed the 10-slot subagent semaphore. It was added when a batch of
taskcalls in one model response was executed withPromise.all, which is no longer how the loop works —taskis deliberately excluded from the parallel-safe set, so sibling task calls run in order, and a subagent cannot delegate further (thetasktool is only advertised when subagent prompts exist, and a child is given none). At most one subagent per session was ever in flight, so the only thing the shared, process-wide limit could still do was make one session's subagent queue behind ten other sessions'.docs/architecture.md's claim that every tool call in a message runs concurrently was stale in the same way, and now describes the read-only-only rule the loop actually applies. - task: a subagent's completed work was thrown away if its transcript couldn't be archived. The archive write sat inside the same
tryas the run, so a failure there replaced the subagent's answer with "Subagent failed with an error".subagent_runs.session_idcarries a foreign key tosessions, so any subagent running in a session whose row wasn't on disk yet reportedFOREIGN KEY constraint failedinstead of its result — verified live, an answer the subagent had produced (and the provider had billed for) was discarded. A full disk or a locked database did the same to the memory-side progress file. Both writes are best-effort now and logged when they fail; losing the archive copy no longer loses the result. - bash: every command a session ever ran was kept in memory, with its output. Nothing removed a finished task from the background-task registry, and on the TUI and web surfaces every foreground bash call goes through that registry — so a session accumulated up to
maxToolOutputBytes(64KB by default) per command for its whole life: measured 3.77MB retained after 200 commands, and hundreds of megabytes per live session for a long one running thousands. The hundred most recent finished tasks are kept (which is whatbash_outputcan plausibly still be asked about) and older ones are dropped oldest-first; a running task is never dropped. - bash: a command that timed out but whose process refused to die left the turn hanging forever. After the timeout's SIGTERM and SIGKILL, nothing resolved the call if the process never reported closing — stuck in uninterruptible I/O, or reaped with its exit lost. The abort path already had this net; the timeout path now does too, and returns the output captured so far. The stray timers both paths left behind (including a heartbeat interval whose body was empty) are cleared on the way out.
- edit: a fuzzily-matched block was written back at the wrong indentation. Several matchers in the chain deliberately ignore indentation when searching — that is what lets an edit land when the model remembers a block without the file's leading whitespace — but the replacement was then inserted verbatim. Verified: replacing a method body inside a Python class dedented
def go(self):out of the class, changing what the program means. The replacement is now re-indented to the level of the text it replaced, in the file's own indent characters (a tab-indented file is not re-indented with spaces), and an exact match is left untouched. - edit: a file with one very long line could exhaust memory. The block-anchor matcher scores candidate lines with a Levenshtein distance that allocated one matrix cell per character pair, so the single 200KB line a minified bundle or generated data file is made of meant billions of cells — reproduced as a hard out-of-memory crash. The distance is computed over two rolling rows and capped in input length, which answers the same "are these roughly the same line?" question at a bounded cost.
- Tests:
edit's matching layer had no tests at all, despite being the code that decides which span of a file gets overwritten. It has fourteen now, covering indentation realignment, the refusal paths (missing, ambiguous, identical, empty), and the pathological-line case. - Prompts: the shared prompts still taught the model an
editAPI that no longer exists. They describedreadas returning "hashline anchors" (it returns plainN: content), told the model to recover from astale-anchorerror the tool cannot emit, pointed at "shared anchor guidance" that isn't there, and instructed it to change several places in one file by passing "multiple ops in oneeditcall" —edittakes a singleoldString/newStringpair and would reject that outright. All six now describe the tools as they actually behave. - Cleanup: removed the unused hashline implementation —
tools/files-legacy-hashline.ts,tools/hashline.ts,tools/hashline-cache.tsand their tests, ~1,900 lines that nothing had called sinceeditmoved to literal text.plan.tswas still invalidating entries in a cache nothing filled. - read: the whole file was loaded into memory no matter how little of it was asked for. Buffer plus UTF-16 string plus split array costs roughly three times the file's size: measured 592MB of RSS and 1.7s to return five lines of a 200MB log, so a multi-GB log, dump or
.jsonldataset took the process down before printing anything. A file over 8MB is now streamed and only the requested window is kept — the same five lines cost 11ms and no measurable memory. Binary detection also moved ahead of the read, instead of pulling a whole blob into memory only to reject it. - write / edit: cast's built-in-UI guard blocked writes in unrelated projects. It was a substring test for
/src/server/public/and/dist/public/against the path alone, so a user working on their own Node app that happens to havesrc/server/publiccould not have the agent write there at all (verified). The guard is anchored to cast's own installation root now, so it protects what it was meant to protect and nothing else. - Background actors: a long-running background actor's lease was never renewed. It was set once when the actor was created and left alone, so after 45 seconds a perfectly healthy checkpoint writer, dream or distill run — all of which routinely take minutes — looked abandoned to any other daemon process: it claimed the actor, marked it stalled and started the same work over, while the original's own final save was rejected by the owner-token check and silently lost. The heartbeat now extends the lease along with the timestamp, and a resumed actor gets a fresh expiry instead of inheriting a stale one.
- Performance: the actor watchdog re-read every historical actor row every 45 seconds, in every process. Measured on a real store: 694ms and 53MB of fork transcripts parsed per pass, all of it thrown away because the rows were already in memory. The periodic pass now asks only for actors a restart could still act on (0.2ms), and a finished actor's fork context — which nothing can ever resume, and which is the bulk of the row — is no longer read at all, so the first load drops from 694ms to 36ms and the daemon stops holding 53MB of dead transcripts for its lifetime.
- ACP:
session/closedeleted the conversation. Closing a thread in the editor erased that session's history from disk — and, when its cwd was the session's own throwaway sandbox, that directory with it — which is not whatclosemeans in the protocol and left thelistandloadcapabilities this adapter advertises useless for anything the user had closed. Close now unloads: the runner is aborted, the session's own MCP connections are released, unsaved state (mode, todos, an aborted turn's messages) is persisted on the way out, and the conversation stays on disk for the nextsession/load. Removing a session for good is/sessionsin cast itself. - ACP: closing one session in the editor tore down the MCP servers every other open session was using. A single
StartupResult— and with it one MCP connection pool — is shared by every ACP session in the process, butsession/closeclosed that pool along with the session's own editor-provided servers, so the remaining threads went on calling tools whose connections were gone. Only the session's own servers are closed now; the shared pool belongs to the process, exactly as the code's own comment already said. - ACP: a paged session listing could silently skip rows or loop. A cursor naming a session that no longer existed (deleted between two pages) produced an empty page together with a
nextCursorcomputed from index −1 — the editor was handed nothing to show plus a cursor pointing somewhere else entirely. An unknown cursor now ends the listing. - ACP: a provider error mid-turn discarded everything the turn had produced. The loop appends assistant and tool messages into the session as it goes, but only the success path persisted them, so the next
session/loadreplayed a conversation that stopped at the user's prompt. The partial turn is saved before the error propagates. - ACP: every approval left a live 60-second timer behind — the timeout in the permission round-trip's
Promise.racewas never cleared, so the process could not exit until the last one elapsed, and a session with many approvals accumulated one such timer per decision. - ACP: the editor's open buffers were injected in full on every prompt with no ceiling, so a few large files open in the editor (a lockfile, a bundle, a long log) could exhaust the model's context on their own and fail the turn with an error the user had no way to connect to the files they happened to have open. The injection now has a character budget, cuts a buffer short rather than dropping it silently, and says how many files it left out.
- Plugins: a marketplace manifest could delete and overwrite a directory anywhere on disk. The
namein a third-party repository'smarketplace.json— and a plugin's name inside it — were used verbatim as a directory component under~/.cast/plugins, on a path that isrmSync'd recursively before being written. A manifest declaring"name": "../../../something"therefore destroyed whatever was at that path during a plain/plugin marketplace add; verified live against a throwaway directory, whose file was gone afterwards. Both names must now be a single ordinary path segment: a traversing marketplace name is refused outright, a traversing plugin name is dropped from the catalog so the rest of a large third-party catalog stays usable, and the paths read back from cast's own plugin state are checked to be inside the tree they belong to before anything is removed. - glob / grep: a result outside the working directory came back as a mangled path. Shortening an absolute path to a cwd-relative one tested
path.startsWith(cwd), which is also true for a sibling whose name merely begins with cwd's — so with cwd/w/proj, a hit in/w/proj-extra/a.tswas reported asextra/a.ts, a path that resolves to nothing and whose follow-upreadcould only fail. Verified live before the fix. Paths genuinely inside cwd are still shortened; anything else stays absolute. The same faulty test governed which files were recorded as context files, and is fixed there too. - web_fetch (local backend): the 5MB response cap could not actually stop a large download. The body was read with
arrayBuffer(), which buffers the whole response, and only then measured — so the check reported a limit that had already been blown, and a server declaring noContent-Length(or understating it) could stream gigabytes and take the process to an OOM kill before the guard it was supposed to be caught by ever ran. The body is now read chunk by chunk and cancelled the moment it passes the cap, so at most the cap plus one chunk is ever held. - web_fetch (local backend): the response was always decoded as UTF-8, ignoring the charset it declares. A page served as windows-1251, iso-8859-1 or shift_jis — still common outside the anglophone web — came back as replacement characters, and the model had no way to recognize it as an encoding problem rather than a broken page. The
Content-Typecharset is honored, an HTML document's<meta charset>is used when the header carries none, and an unknown label falls back to UTF-8 instead of failing the fetch.
0.23.0
Fixed
- Web: a queued message the daemon delivers after a turn ends (a stranded steer, a follow-up) was submitted as a detached promise with no rejection handler.
submitthrows for a session no longer loaded — closed, deleted, or evicted in the window between the delivery being armed and firing — and Node's default for an unhandled rejection is to terminate the process, so one undeliverable message in one session would take the daemon and every other live session down with it. The failure is now caught, logged, and reported to the session as a notice. - Database: migrations are identified by name, not by version number. Two lines of this codebase evolved the same schema and assigned the same numbers to different migrations, so a store carrying the other line's 29-32 treated this line's 29 as already-applied and its work silently never happened — the warning added earlier could report that, but not fix it. A migration now runs when its name has never been recorded, and is recorded under whatever version number is free; on the affected store this immediately applied the migration that had been stuck since August. Every
up()is idempotent, so a re-run against a schema that already has the change is a no-op. - Database: removed
messages_fts, a second full-text index over textsession_history_ftsalready covered. The two held the same message bodies —messages_ftswas exactly the user/assistant subset, and the surviving index carriesrole, so the same searches are answered from one place (verified identical result sets on a real store). Every message write had been updating both indexes, and the redundant copy was ~11MB of a 547MB database. Its triggers and the seq-sync repair that maintained it go with it. - Memory: a memory's fingerprint was taken from the raw
typewhile the row stored the trimmed one, so" fact"and"fact"produced different keys for rows that are identical once stored — slipping pastUNIQUE(project_id, fingerprint)and duplicating an entry. - Auth: the daemon token was compared with
===while the web password next to it used a constant-time comparison. Both now go through the same constant-time check; loopback-only either way, but the inconsistency is gone. - Plan mode: a command reading its arguments from a file (
strings @opts, the binutils convention) bypassed the per-binary flag scan entirely. Harmless forstringsitself, which has no dangerous flag, but it would have silently defeated the whole check for any binutils tool added to the read-only allowlist later. - Web UI: the turn-finished chime played even while the tab was in front and the user was watching the turn happen — the desktop notification beside it was already gated on the tab being hidden, the sound wasn't. It also built a fresh
AudioContextper turn, which accumulates against the browser's per-page limit until later chimes silently fail; one shared context is reused now. - Sessions: dropped the vestigial
checkpoint_watermark_seqplumbing. The durable watermark moved to an immutable message id in migration 19, and nothing had written or read the old column since — it was still being carried through every session insert and upsert, including a MAX() merge that could never fire. - MCP: an MCP tool call was capped at the SDK's 60 seconds, a limit that was neither reachable nor documented — a slow but legitimate tool (a browser step, a heavy query) simply failed with a timeout the user could do nothing about.
mcpToolTimeoutSecondsnow sets it (clamped 5–3600, unset keeps the 60s default), read per call so a change applies without reconnecting, and documented in docs/configuration.md. - Hooks: a matcher's
.meant two different things depending on the rest of the string — a single dotted name likemcp.foowas compared literally, whileEdit.*, one character longer, was a regex that matchedMultiEdit. Two matchers that look alike behaved in opposite ways. A dot is now a regex "any character" in both, while a|/,list stays a list of literal names so file matchers likepackage.json, README.mdkeep working. The matcher rules — including that a regex is unanchored, and that some events have nothing to match against — are now written down in docs/hooks.md. - Hooks: the TUI's
/compactfired neitherPreCompactnorPostCompact, so a guard written to protect a long transcript was honoured by the web/compactand the automatic threshold but not here, and a bookkeeping hook never saw a manual TUI compaction. Both now fire, and a blockingPreCompactstops the compaction before it starts rather than after. - Hooks: the
Notificationevent — declared, matcher-aware and documented as part of the supported set — was dispatched from nowhere, so a hook written to ring a bell, post to Slack or flash a window never ran. It now fires at the two moments that actually warrant interrupting someone: a turn ending (successfully or with an error) and the agent blocking on the user, withnotification_typeset toturn_completeorinput_needed. The five events cast genuinely cannot fire (Setup,TeammateIdle,Elicitation,ElicitationResult,ConfigChange) are no longer listed in the code as implemented — the docs already said so, the module's own comment didn't. - MCP: a server that drops now comes back on its own. Recovery used to be a manual
/mcp reconnect, which closes and re-resolves every server — fine as a deliberate action, far too blunt to automate, since one flaky server would take the rest down with it on each attempt. A dropped connection is rebuilt in place instead, leaving the others untouched, with five bounded attempts backing off from one second to sixteen; after that cast stops retrying and leaves the server visibly disconnected. A connection cast closes on purpose (shutdown,/mcp disable, a manual reconnect) is never retried. - MCP: a server dying mid-session went unnoticed — the SDK's
onerror/oncloseare no-ops unless assigned, and nothing pruned the connection list. Its tools stayed in the system prompt for the daemon's lifetime, so the model kept calling them and kept failing,/mcpstill reported it as connected, and the only clue was a transport error. A disconnect is now detected and logged, the server drops out of the prompt and the/mcplisting, and a call to it explains that it needs/mcp reconnectinstead of surfacing a raw error. - MCP: a turn sent while the daemon was still connecting its servers in the background ran without any of their tools and said nothing about it. It now says so, naming the servers.
- MCP: two different (server, tool) pairs can sanitize to the same tool name — every character outside
[a-zA-Z0-9_-]becomes_, so a server calledgithub.apiwith toolxcollides withgithubandapi_x. The lookup was last-wins while the tool list kept both, so the provider received duplicate function names and calls routed to whichever server happened to connect last — different between runs, with nothing reported. The first now wins deterministically and the collision is reported as a diagnostic. - MCP: the daemon's shutdown never closed its MCP connections, relying on stdin EOF to end stdio servers once cast exits — a server that ignores EOF survived, leaving an orphaned child process behind every restart. Every other path already closed them explicitly.
- Hooks:
hookSpecificOutput.additionalContext— the field the docs document for adding context to a prompt — was parsed, merged and unit-tested but never read by any caller, so it did nothing. What actually reached the model wasreason, which is only populated on block-shaped output.additionalContextis now used, withreasonas the fallback, capped at 8000 characters, and with a closing</hook-context>in hook output neutralised so it can't end the block early and have the rest read as cast's own words. - Hooks: a matcher on an event that has nothing to match against (
Stop,WorktreeCreate,MessageDisplay, …) is ignored and the hook runs every time. That behaviour is unchanged — changing it would silently stop existing hooks from firing — but it is now reported once per event+matcher, since a matcher that looks like a filter and filters nothing is the opposite of what the author intended. - Worktrees: the
WorktreeCreatehook is documented as able to cancel a creation, but it fired from exactly one of the four places that create a worktree — the TUI's/worktree, the--worktreeCLI flag andPOST /api/sessions {worktree}all went straight to the worktree helper and never asked.WorktreeRemovelikewise never fired from the TUI. Both hooks now live inside the shared creation/removal path, so every surface honours them by construction and a new call site can't quietly skip one. A hook refusing a creation from the CLI now prints what the hook said instead of a stack trace. - Hooks: a
PreCompacthook could not cancel automatic compaction, though it is documented as able to cancel compaction and the manual/compactpath honoured it — the automatic path awaited the hook and discarded its answer. A guard written to protect a long transcript therefore worked everywhere except the one path that fires on its own. - Hooks: a hook that failed to run at all — a typo'd path, a missing interpreter, no execute bit — was indistinguishable from one that ran and approved: the exit code was recorded and read by nobody. Such a hook is now reported (with its exit code and any output), while still not blocking the turn. Verified on a live daemon:
PreToolUse hook exited 127 with no output — it did not block, and its result was ignored. - Hooks:
timeoutwas honoured only by command and http hooks. Anmcp_toolhook waiting on a wedged server, or aprompthook waiting on a stalled provider, blocked forever — and with it any tool call the hook was gating. Both are now bounded by the same timeout and reported when they hit it. - Compaction: a summarization call that succeeded with empty content still compacted — every superseded message left the working context and was replaced by a content-free marker, while the caller reported success. Providers produce this in ordinary ways (a reasoning-only stream, a refusal emitted as an empty assistant turn, a stream truncated without throwing). It is now treated like any other summarization failure: the history is kept and compaction retries on the next turn.
- Turns: the near-the-budget reminder was pushed on each of the last four iterations rather than once on entering that stretch, spending context on near-identical system messages exactly when the turn is short of room.
- Turns: requesting a per-run iteration budget against a session whose turn is already running dropped the budget in silence. The message still steers into the running turn, but a notice now says the budget applies to a new turn rather than this one.
- Agents: creating an agent failed outright on a database carrying the multi-tenant
agents.user_idcolumn —NOT NULL constraint failed: agents.user_id— so the feature was simply unusable on such a store (found on a real one). The row is now attributed to an existing user when that column is present. - Agents: persona and provider were stored verbatim with no validation (
PATCHeven acceptedpersona: ""), and the spawn path then resolved an unknown persona to the global default and an unknown provider to the global endpoint silently — so an agent pinned to a provider the user later renamed ran somewhere else entirely, with only a confusing provider error later to hint at it. Both are validated at save time now, with the list of valid values, matching what/personahas always done; a session spawned from an agent whose pin has gone stale fails with 409 instead of quietly running elsewhere. An unknown model is still allowed on purpose — the provider's model list is remote and may be stale. - Steering (message loss): a steer that arrived between the loop's last queue drain and the turn going idle was stranded — nothing drained that queue again, so the message was never delivered and the "Steer queued" chip stayed up forever. Follow-ups already had a net for this; steering didn't. It now gets the same handoff, and both nets also run when a turn ends in an error, where neither used to.
- Steering (message loss): a steer already taken off the queue was thrown away when the turn hit its iteration budget — not persisted, no event, gone without trace. It's now kept in the conversation, with a notice saying it will be answered on the next turn.
- Plan mode: approving a plan now switches the session to build in the same call. The client used to resolve the approval and then separately switch the mode, so anything interrupting it in between — a closed tab, a dropped connection, a daemon restart — left the approval consumed and the session still in plan mode: the approval card gone, the model still read-only, and nothing left to approve again. Reproduced against a live daemon.
- Memory tool: asking it for an operation it doesn't have (
operation: "store"— it only searches; durable memory is written for you at the end of a turn) answered with a plain empty search result, which reads as "your write didn't take". A real run took that at face value and went off editingMEMORY.mdby hand. It now says what the tool does and doesn't do. - Memory: an entry's
importance/confidencewere dropped when the model sent them as strings —importance: "95", which is what models routinely emit — silently replacing them with defaults, so a rule asked for at 95/90 was stored as 90/50. - Memory (data loss): the project
MEMORY.mdfile is the canonical store and the database rows are its projection, but thememorytool wrote rows only to the database — so everything the model ever stored was deleted by the next reconcile, the moment anyone searched memory or the file changed. Storing now writes through to the file, with the metadata a bullet can't otherwise carry (exact type, confidence, expiry) kept in a trailing<!-- cast: … -->comment that renders invisibly. Rows written before this are lifted into the file on the next reconcile instead of being dropped. - Memory: only the first eight bullets of a memory file were ever indexed — the eight-entry cap on a single model response was also being applied to the whole canonical file, so everything below the eighth bullet existed on disk but could never be searched or injected. On this machine that was 8 of 18 bullets in one project.
- Memory: reconciling an edited file deleted every row for the project and re-inserted them, so a one-line edit renumbered every entry and re-tokenised the whole search index. It now merges: the file still decides which entries exist, but an unchanged bullet keeps its row, its id and its metadata.
- Memory: the raw file text was placed in the prompt ahead of the ranked entries it duplicates, so a small budget spent itself on unranked file content and dropped the entries that were actually retrieved. Ranked entries go first now, the file into whatever budget is left.
- TUI: a streaming reasoning block never drains into the static region mid-turn, and the live-region clamp re-measured its entire text on every frame — quadratic over the turn. Measured on a 371KB reasoning stream across 1500 frames: 1039ms of clamp work, worst frame 9.5ms; a 244KB single-line stream cost 1507ms with a 12.7ms worst frame and held 137MB of heap in the width cache. The clamp now measures from the end and stops once the remaining rows are accounted for, and width measurement gives up past the budget: 328ms / 3.9ms worst and 344ms / 3.8ms worst for the same streams, with the cache bounded so it can't retain a turn's worth of ever-changing tail lines.
- Web UI: the scroll-up page cache was an unbounded Map cleared only by
/clear, so a tab left open for days kept the loaded history of every session ever visited — walking to the top of a 4000-message thread pinned all of it for the life of the tab. It now keeps the three most recently used sessions, and drops a session's pages when that session is deleted. - Database (data loss risk): a version recorded under a different name than this build expects was skipped in silence, so whatever that migration does never happened. A real store had version 29 recorded by another line of this codebase (
users-and-multi-tenant-columns), which meant this line's 29 — the one that adds themessages_fts_autrigger — was treated as applied while the trigger was missing, leaving exactly the stale-search-index bug it was written to fix. Mismatches are now reported loudly, and the trigger is restored by a repair migration under a free version number. - Database: actor fork snapshots serialized the same transcript three times over (
messages,inheritedMessages, andprefix+tailare all views of it) — 140MB of a 547MB store, the largest single row 21MB holding 7.8MB of distinct data. Only the transcript and its boundary are stored now, with the views rebuilt on read; existing rows are compacted on upgrade (140MB → 53MB here). Rows in the old shape still read correctly. - Database: recorded session events (
tool_start/tool_end/turn_endexecution telemetry, reachable only through the events/history endpoint) had no delete path beyond the cascade on session delete, growing roughly two rows per tool call forever — 8377 rows and 60MB of payloads on one real store. They now expire on the same 7-day window as the rest of the telemetry. - Performance: every message the web UI sent re-read and JSON-parsed the entire session history to check one field — the client-message-id dedup that makes a reconnect's re-send idempotent. Measured 62ms per send on a 4465-message session and growing linearly with history. It's now a bounded SQL lookup over the tail of the session (0.4ms), which is all a re-send can ever race.
- Web UI: the streaming block scheduler stored both a frame id and a timeout id in one ref and always cancelled it as a frame, so a pending throttle timer survived reset/take/unmount and fired a stale flush afterwards — and the id it passed to
cancelAnimationFramecould cancel an unrelated pending frame that happened to share the number. - Web UI: the retry-row cleanup allocated a copy of the whole loaded transcript on every token and thinking event, then usually discarded it unchanged. It checks first and only rebuilds when there's actually a row to drop.
- ACP: the editor-facing session listing loaded every message of every session off disk to build rows that only need an id and a cwd — 1.4s on a 600-session store, on every listing. It reads the same rows from the summary aggregates instead (42ms), which are also ordered newest-first, so the listing's cursor paging now walks a deterministic order rather than whatever order the rows came back in.
- Performance: deleting a session took minutes on a large store. Both message search indexes keep
session_id/seqas FTS5UNINDEXEDcolumns and the per-messageAFTER DELETEtriggers delete by them — which FTS5 cannot index, so each of them scans the whole index, once per message. Measured on a real 185MB store: removing one 2786-message conversation took 241 seconds, holding the write lock the entire time, long enough for every concurrent writer to blow past the 5sbusy_timeout. Deletes now clear each index with a single session-scoped statement instead, with the triggers dropped for the duration and replayed fromsqlite_masterafterwards: the same delete takes 0.3 seconds. - Sessions: background sessions — cast's own working snapshots from checkpoint writers and memory dream/distill runs — were never deleted by anything. They don't appear in the sidebar or the picker, so they accumulated invisibly: 376 of 999 session rows and 23% of the stored content on one real installation. They now expire after 7 days, swept in bounded batches after the daemon starts listening (so neither daemon startup nor a concurrent writer waits on the sweep). A user's own conversations are never pruned, however old.
- Tests: a checkpoint writer is fire-and-forget, so it outlived the test that started it and reached the database after
afterEachhad already restoredCAST_SESSIONS_DB— writing background sessions into the developer's real~/.cast/sessions/sessions.db(358 rows had accumulated there). The two tests now await their writers, and opening the real database from a test run is refused outright, so a future leak of this kind fails loudly instead of silently polluting real data. - API v1: a request naming a session that doesn't exist answered 400 on
/mode,/plan-transition,/questionand/clean-context, sharing a status with genuine bad-request errors — sostatus === 404, the documented contract and the only way an integration can tell a deleted session from a malformed request, never fired. It's a 404 now. - API v1: the OpenAPI document drifted from the handlers in several places that would break an integration written against it:
limit/offsetonGET /sessionswere undocumented and switch the response from an array to a paged object;provider/agentIdon session creation,goalon chat, and multi-select answers on/questionwere missing;DELETE /sharewas declared as alwaysok: truewhen it answersok: falsefor a session that wasn't shared; and the 404s on rename/pin/share/create and the 500 on chat weren't listed. - Plan mode: the write/edit gate matched the plan file lexically (
dirname+.md) andwritefollows symlinks, so a link planted at the plan path during an earlier build-mode turn — or by a prompt injection during one — redirected a write in "read-only" plan mode to wherever it pointed, after the user switched modes trusting nothing would be touched. The gate now resolves symlinks and refuses a plan path that is one. - File browser:
isInsideRoot, the single containment check behind every/fs/*route, was purely lexical, while every consumer (statSync,createReadStream,rmSync,renameSync) follows symlinks — so a link inside the session's cwd let the browser list, download, rename and delete outside the project, which is the one thing the check exists to prevent. Paths are now compared resolved (a not-yet-created destination by its nearest existing parent). - Daemon: the event-recording side of
onEventran unguarded, andloop.tscallsonEventfrom around sixty places without a try/catch — including from the handler that reports a failed turn, so a database fault there escapedrunAgentLoopentirely instead of just losing a telemetry row. Recording is now best-effort and logged. A failedsaveSessionin the message-persistence callback is logged too, rather than swallowed in silence. - Sessions: a session killed mid-tool-call (SIGKILL, an OOM — SIGTERM/SIGINT already write aborted results) was bricked permanently. The assistant message with its
tool_callsis deliberately persisted before the tools run, so it sits on disk for the whole call, and every provider rejects a history containing a tool call with no matching result — so every later turn in that session 400'd, with no way to recover from inside cast. Unanswered tool calls now get a synthetic "interrupted" result on the request path, which also repairs sessions already left in that state. - Database:
saveSessionwrote the session row (bumping its version) and each of the turn's messages as separate statements with no transaction around them, so a failure partway left the row claiming a version whose messages were only half persisted. The whole save is now oneBEGIN IMMEDIATEtransaction, and a message is only recorded as persisted in memory once that transaction commits. - Database: the pending-migration set was read outside the transaction that applies each migration, so two processes opening the database during the same upgrade both saw a migration as pending — the second one's bookkeeping insert then failed on the duplicate version and threw out of
getDb(), taking the process with it. Several processes migrate independently (the daemon, a TUI picker,cast run, an ACP connection), and the window is seconds wide after an upgrade that rebuilds the search index. The check now happens inside aBEGIN IMMEDIATEtransaction, which also means a concurrent migrator waits outbusy_timeoutinstead of failing instantly as a deferred transaction did. - Database: the connection singleton was published before migrations ran, so a failed migration left every later
getDb()handing back a partially-migrated connection with no retry — the real cause then surfaced much later as a confusing "no such column". It's now published only after the connection is fully initialised. - Sessions: one session row with a malformed JSON column (a partial write, a hand-edit) threw out of
listSessions, hiding every session from the sidebar and the picker. The unreadable row is now skipped with a message on stderr, so the rest of the list still works. - Settings: a
settings.jsonwith a syntax error read back as{}and the nextupdateSettingswrote that empty state out as the whole file — every provider entry and API key gone, no warning, no way back. It took no user action to trigger: the daemon callsupdateSettingsduring an ordinary turn. The unreadable file is now moved aside assettings.json.corrupt-<timestamp>(with a message on stderr) before defaults are written, so its contents stay recoverable. - Plan mode: two more writers on the read-only allowlist —
file -C -m srccompiles a magic source and writessrc.mgc, andyq --split-expwrites one file per result regardless of-i. Also refuses ugrep's--filter/--view/--pager/--save-config, sincegrepis commonly a drop-in for ugrep and--filterisrg --preby another name. cast run: a turn cut short exited 0 with a silently truncated answer on stdout — onlyreason: "error"was treated as failure, so the provider dropping the stream mid-response ("disconnected", which exists specifically to mark a cut-off answer) looked like a clean run toout=$(cast run …). Any reason but a clean stop or a user abort now exits 1 and says so on stderr. One-shot runs also droppednoticeevents entirely, swallowing the runaway-loop iteration cap,/goalbudget exhaustion and model refusals — all of which end the turn with"stop"; they now reach stderr, as they already did on the interactive path.- Plan mode (security): plan mode promises no code execution, but
rgis on its read-only allowlist and ripgrep's--preruns an arbitrary command per searched file (--hostname-binruns one outright). Since plan mode also lets the model write any.mdinto the plans directory,rg --pre=sh q <plan>.mdexecuted that file's contents at full user privilege — a complete escape from the read-only restriction. Both flags are now refused. - Telemetry: every
/api/*request was recorded under its concrete URL, so a shared thread's token — the only credential guarding an otherwise-unauthenticated read — was persisted intoapi_requestson every anonymous view and rendered in the dashboard's endpoint table. Requests are now recorded under the matched route's template (/api/shared/:token), which also stops each session id becoming its own row in the endpoint overview. - Sessions (security):
DELETE /api/sessions/:id/permanentpassed the URL's id straight intormSync(join(~/.cast/inputs, id), {recursive, force}). Node never normalizes..out ofreq.urland the router's([^/]+)matches it, soDELETE /api/sessions/../permanentrecursively deleted the entire~/.castdirectory — settings with provider keys,sessions.db,keys/,skills/,plugins/,memory/— and answered a misleading 404. Session ids are now validated as a single safe path segment, both at the delete entry point and insidesessionInputsDiritself. - Auth (security): a discovered factory UI whose directory name matched a daemon route's first path segment disabled authentication for every route under it.
mkdir ~/.cast/ui/api(a plain directory with anindex.html, nocreateUicall and therefore no reserved-name check) made the whole API answer unauthenticated,POST /api/sessions/:id/chatandPOST /api/settingsincluded. The reserved-slug list now lives in one place and is enforced at discovery, so such a directory is never registered as a UI at all. - Plugins:
GET /api/plugin-content?id=joined both halves ofname@marketplaceas path segments without checking them, so..@..read outside the installs directory and the not-found error echoed the resolved absolute path. - SSH keys (security):
POST /api/ssh/keywrote the uploaded key to~/.cast/keys/<name>with the client-supplied name used verbatim — nobasename(), no traversal check — so a name like../../.ssh/authorized_keysresolved outside the keys directory and turned the endpoint into a write-anywhere-the-daemon-can-write primitive. Now applies the samebasename()rule the attachment routes already document. - Reasoning: the system prompt told the model the globally configured reasoning level and supported-effort list, while the request itself carried the level resolved for that run — a session pinned to a provider with a different reasoning vocabulary read "Reasoning: high" in its prompt on every turn while actually running at "enabled". Both lines now come from the run's own resolved values.
- Server: every JSON POST route buffered the entire incoming request body into memory with no ceiling. The attachment-upload route's 25MB limit only applied after the whole (base64-inflated) body was already resolved and parsed, so a mistaken multi-gigabyte drop — or any client sending one — ballooned the daemon's heap before being rejected. Bodies past 48MB (headroom over a 25MB attachment's base64 encoding) are now refused with a 413 while still streaming, with the remainder drained rather than retained.
- Plugins:
/plugin install's git clone can take seconds — a settings change from a concurrent command or another tab landing in that window was silently overwritten once the install finished, since itsenabledPluginswrite was derived from the settings snapshot read before the clone started. Now re-derives just the installed plugin's own enabled flag against current settings at write time. - ACP: the ACP (editor/IDE) integration leaked a client connection reference on every session, for the life of the daemon process —
closeSessioncleaned up the runner, MCP connections, and open-document buffers, but never released the module-level map tracking each session's client, populated on every prompt. Any long-running ACP-connected editor (e.g. Zed) opening and closing many chat sessions over a work session accumulated these indefinitely. - Search: compaction's marker-insertion could shift a kept message's row to a new position without updating its search-index entry (the index only synced on insert/delete, not on this seq shift) — a search hit on that message's content could resolve to the wrong row, and the stale entry was never cleaned up, silently accumulating dead rows in the index every time a session compacted. Existing databases get a one-time rebuild on upgrade.
- Dashboard: the "avg messages/session" stat ignored whatever time window was selected and always averaged over the entire database's history, shown right next to a session count that was correctly windowed — picking a short range (e.g. "last hour") could show a small, correct session count next to an unrelated all-time average from months of history.
- Telemetry: only the
llm_requeststable had retention pruning —tool_calls,api_requests,compactions, andmemory_maintenancegrew unbounded for the life of the database.api_requestsin particular logs one row per HTTP request the daemon serves (page loads, polls, SSE reconnects), the highest-volume of the five. All five now share the same 7-day retention. - Provider pinning: a session pinned to its own saved provider got silently reconciled against a completely different provider whenever the global active endpoint changed elsewhere (another session's
/providerswitch, the TUI, or a manual settings.json edit) — if the pinned session's model wasn't served by that unrelated provider, it got reset to that provider's default model, defeating the whole point of the pin. A pinned session's model is no longer touched by a global endpoint change that isn't its own. - Plan mode: switching mode by any path other than the approval card's own buttons (the mode toggle, or
/plan//build) while a plan question or approval was still pending left it dangling — the stale card could still render afterward, and answering it would run the approval against a session already in the other mode. Mode switches now clear any pending plan question/transition. - Settings: the Memory, Quick Mode, Plugins, and Marketplace tabs (and Skills.sh) silently rendered an empty list when their data failed to load, indistinguishable from actually having none configured — a marketplace with an unreachable git remote, for example, looked identical to "no marketplaces added." They now show the same error banner the other tabs already did on a failed fetch.
- Model tab: the reasoning-level shown in
/current(the Settings → Model tab header) was always the raw global level, even for a session pinned to a provider whose model doesn't support it — e.g. a global level of "high" next to a session pinned to MiniMax (whose reasoning vocabulary is enabled/adaptive/disabled) showed "high" even though the next turn would actually run with "enabled". The turn itself was already resolving this correctly; only the display was stale.
0.22.31
Fixed
- Compaction: the web UI / daemon-backed path never seeded
lastPromptTokensinto a fresh agent run, so the automatic-compaction check was blind for every turn after a session went idle and a new one started — a large session could silently skip auto-compaction turn after turn instead of proactively summarizing, risking a raw "context exceeded" error from the provider instead. Now seeded from the session's own persisted token count on every run. - Rules: the nested-AGENTS.md/CLAUDE.md fix in 0.22.30 tracked touched files per
runAgentLoopcall instead of per session — a match found in one submit could drop back out of the prompt on the very next submit once the session went idle in between. Now persisted on the session like the sticky auto-rules already were. - Overnight/long-running web sessions never got the day-rollover reminder (
<system-reminder>noting the calendar date advanced) that the standalone TUI already had — wired the same way now.
0.22.30
Fixed
- Rules: nested
AGENTS.md/CLAUDE.mdfiles (in a subdirectory the agent touches, not the project root) had the same gap as the directory-rules fix in 0.22.29 — implemented and wired into the standalone TUI, but never reaching the web UI or a daemon-backed TUI session. Now injected there too, per-turn, once a file from that subdirectory enters context.
0.22.29
Fixed
- Rules:
applyMode: "auto"directory rules (glob-matched, latching once a matching file enters context — Cursor-style sticky rules) and@name-mentioned rules were fully implemented and tested but never wired into the web UI / daemon-backed session path — only the standalone TUI (no-daemon mode) actually used them. A rule authored withglobsnever reached the model through the web UI or a daemon-backed TUI session, silently. - Rules: a nested
alwaysApply: truerule (e.g.apps/web/.cast/rules/style.md) was supposed to only apply once a session touches a file under its own subtree, but the per-turn rules formatter injected every always-apply rule from the whole project into every session regardless of scope — contradicting its own documented behavior. Fixed at the root: the formatter now renders whatever the (already scope-gated) sticky rule set contains, instead of independently re-scanning the full catalog.
0.22.28
Fixed
- Security: the
web_fetchtool's "local" backend (an opt-in alternative to the default Jina Reader proxy,/web-fetch-provider local) fetched the model-supplied URL directly from the daemon's own process with no restriction on the target — a model could be steered into fetching169.254.169.254(cloud instance metadata),127.0.0.1(the daemon's own API — though still blocked by its normal auth), or any other address on the host's private network, and get the response back as tool output. Now refuses any URL (including a redirect target, checked on every hop, and a hostname's DNS-resolved address, to catch rebinding) that resolves to a loopback, link-local, or RFC1918 private range, before ever issuing the request.
0.22.27
Fixed
- Plugins: a plugin's git clone getting interrupted (process killed, network drop) left a
.gitdirectory that existed but couldn't be fetched or pulled from — every later install attempt hit the same broken repo and failed forever, with no way to recover short of manually deleting the directory. Install now falls back to a fresh clone when updating an existing checkout fails. - Plugins: two installs targeting the same not-yet-cloned plugin at once (two tabs, a marketplace reload racing an install) could run two concurrent
git clones into the same destination, corrupting it. These now serialize per destination.
0.22.26
Fixed
- Worktree:
/worktree remove <name>had nothing stopping it from removing a worktree another live session (a second tab, or another session entirely) still had as its cwd — including mid-turn, while a tool was actively reading/writing inside it.git worktree remove --forcebypasses git's own uncommitted-changes guard, so this could force-delete real uncommitted work out from under a running turn. Now refuses with a clear error when any live session still points at that worktree. - Usage/cost tracking: aborting a turn mid-stream could silently drop real, provider-billed token/cost data — a provider can send a terminal usage chunk just before the connection actually tears down on abort, and that already-arrived usage was thrown away instead of recorded, under-reporting cost for any turn aborted after usage info arrived but before the stream's natural end.
0.22.25
Fixed
- MCP: two
/mcp reconnect|enable|disable|uninstall(or/reload) calls in flight at once — two browser tabs, or a fast double-click — each closed and reopened the whole MCP connection set independently; whichever finished last simply overwrote the other's result, leaking the other's freshly spawned MCP server subprocesses with nothing left referencing them to close them. These now serialize, so a second call only starts once the first has fully closed/reopened. - Settings:
/mcp,/hooks, and/skillsenable/disable now read the current settings from inside the same lock they write under, instead of reading beforehand — hardening against the same class of lost-update race for any future code path that adds anawaitbetween the read and the write.
0.22.24
Fixed
- Fork: forking a session with an attachment left the fork referencing the original session's attachment file, not its own copy — an attached file's path is embedded as text in the message that referenced it, not looked up by session id at read time. Deleting the source session afterward (Delete → permanently) removed that file out from under the fork, silently breaking a reference that still looked valid in its transcript. The fork now gets its own copy of every attachment, with its history rewritten to point at that copy — a fork is now a fully independent snapshot, matching how its message history already behaved.
0.22.23
Fixed
- Files/Changes panel: a renamed file (
git mv, or a rename picked up by an editor) showed up as the entire file being freshly added, with no indication it was a rename and no old path — a single-pathspecgit diffhas nothing to compare the new name against, so it can't detect the rename. If the rename also had further unstaged edits on top, the staged half (the rename itself plus any staged content change) was dropped from the diff entirely, silently hiding real uncommitted changes. Renamed files are now diffed against both their old and new path so git's own rename detection populates the header correctly, and the staged and further-unstaged halves are now both shown as separate entries, matching how a plain modified file's staged/unstaged diffs are already handled.
0.22.22
Fixed
- Security: clicking an uploaded attachment in the composer's attachment panel opened it in a new top-level browser tab. For an
.svgattachment, that meant the browser rendered it as a full document rather than an embedded image — so an SVG containing a<script>executed with the app's own session cookie. Attachments now open through the same in-app preview modal already used for the Files panel, which renders images via an<img>tag (SVG scripts never execute there) instead of navigating to the raw file. - Security: the file preview modal (Files panel and, as of this release, attachments) rendered a Markdown file's content through
markedstraight into the page with no sanitization, so a.mdfile containing raw HTML (a<script>tag, an<img onerror>) executed when previewed. Output is now run through DOMPurify before rendering.
0.22.21
Fixed
- Session: resuming a session outside the web daemon (the TUI without a live daemon, or the ACP integration) reset the provider it runs against but left the newly-added per-session provider name stale, pointing at whatever it had been pinned to before. Reopening the same session through the web UI afterward could silently route it back to that old pin instead of the provider actually in use.
0.22.20
Added
- Memory: both project and global memory are now always loaded in full into every session's system prompt when they have content — not just pointed at by path, and no longer left to the model to proactively decide to search for. A saved fact like "this project uses SQLite, not Postgres" or a cross-project preference like "always write commit messages in English" is now simply present from the start of every relevant session, like ChatGPT's saved memories. Bounded by the same
checkpointPushCapssettings (Settings → Memory → Caps) already used for checkpoint-rebuild context, so an oversized memory file doesn't silently tax every turn. Session checkpoint/notes/task-progress are unaffected — still pull-based, fully loaded only at an actual checkpoint rebuild.
Fixed
- Memory: a preference saved to global memory got no proactive recall nudge on a fresh session in a different project — only project-scoped memory triggered the "search memory before asking" reminder. Global memory now triggers it too (on top of the always-loaded content above).
0.22.19
Added
- Web: the New Session modal's provider picker actually works now — a session can be pinned to a specific saved provider, at creation or later via
/model-selection, independent of whichever provider is globally active. Previously the choice was silently dropped before it ever reached the server, and the main model's endpoint was resolved from one config shared by every session, so switching provider in any session immediately redirected every other open session's next turn too.
Fixed
- Web: two saved providers sharing a base URL (different keys) could have a pinned session silently routed to the wrong one's credentials — sessions now record which provider by name, not just by URL.
- Web: Settings panel list actions (MCP/Skills/Hooks/Plugins enable-disable-uninstall) now show per-row pending feedback instead of relying on the modal's global busy flag, which flashed too briefly to tell whether a slower action actually did anything. Memory's numeric fields (checkpoint reserved/thresholds/caps) no longer clear a typed value before the save is confirmed, so a failed save leaves it there to retry instead of silently discarding it.
0.22.18
Fixed
- Server: a second server process (e.g. a dev-mode instance started outside the
cast serverCLI, which already refuses this) no longer silently overwrites~/.cast/server.jsonregistration for an already-running daemon, and no longer erases it on its own shutdown —cast server status/stopcould otherwise start reporting "not running" or pointing at the wrong process while the real daemon kept running untracked.
0.22.17
Fixed
- Web: production static assets are now actually cache-busted after a deploy. The
?v=…stamping regex required a space afterfromthat esbuild's minifier strips, so every local JS import exceptnew-session-modal.jssilently kept serving its pre-deploy body for up to an hour; the PWA service worker's cache name was also hardcoded and never rotated, so a cached module could be served forever regardless of HTTP caching. Both now update on every release.
0.22.16
Fixed
- Web UI: sidebar session item menu now closes on scroll/resize instead of drifting from its anchor row — its position was a one-shot snapshot taken on open that never tracked the row afterwards.
0.22.15
Fixed
- Web UI: sidebar session item menu (⋮ / right-click) is rendered once at the
<nav class="sidebar">level instead of nested inside each row/session-group. Those containers usecontent-visibility: autofor list virtualization, which made the menu'sposition: fixedmis-place and clip against a later group, or — after an earlier attempted fix — forced a relayout that jumped the whole list's scroll position when opened near the bottom.
0.22.14
Fixed
- TUI:
/newand/clearnow repaintCASTascii banner (onRepaintHistoryfull clear +Staticreplay) like a fresh launch;ChatLoghard-cutno longer leaks]<]minimax[>//think>— strips<think>and advances cut to next>boundary (verified800positions0dirty,10clamp tests).
0.22.13
Added
- Skills:
presentation-builderpromoted from global tobuiltin(5presetsminimal/corporate/bold/technical/editorialwith PNG verify gate,166+416+138lines) andforge-reviewgeneralized fromglab-mrto all forges (GitLabglab/ GitHubgh/ Giteateawith threaded reply gate,94lines) —13builtins total.
0.22.12
Changed
- Skills: replace duplicated
super-research(8 heavyweight modes) with provenobra/superpowerstrio —tdd(red-green-refactor, seams),systematic-debugging(4-phase root cause),verification-before-completion(evidence-before-claims gate) — allbuiltinprompts/skills/with0diagnostics;deep-researchremains single research skill,learn-everythingkept.
0.22.11
Fixed
- Appearance flicker:
GET /api/themesnow hydrates fromlocalStorage cast:themesinstantly; fresh list overwrites in background —Appearancetab paints with18swatches on first frame, noLoadingflash.
0.22.10
Fixed
- Settings
gt/ltdisplay:Custom CSShint andDefault UIhint now show<style id="cast-custom-css">and/ui/<name>/correctly via JS expression (was<double-escaped to&lt;).
0.22.9
Fixed
- New session: provider list without
auto(explicitminimaxetc.), verify↻(POST /api/provider/verify {provider}) on any pair, stacked provider/model, sandbox default,1100×704modals (like dashboard) with sticky footer,· activeremoved,Loadingcompact. - Performance:
GET /api/models?provider=no longer blocks modal open (708ms → 330ms, lazy on switch),GET /api/sessionspaginated50/page(585 total 172KB → 3.5KB) withLoad more+ guard. - Reasoning:
showReasoningdefaulttrue(parity TUI/web),LiveStreamingBlocksstreaming viacollapseMidWordBoundaries. - PWA/offline:
manifest.json+sw.js(shell cache, network-first for/api/),index.htmlmanifest+register,turn_end(sse-events.js:204)Notification+880Hzbeep whendocument.hidden. - Theme:
AppearanceCustom CSS(cast:customCss<style id>+storagesync) and cross-tabcast:themeColors/cast:ui:<name>:themesync (factorylib/components.js). - Settings: paddings unified
8px, loaders unifiedLoading(capital, no spinner, centeredflex:1 min-height:120px),Updatesloading centered.
0.22.8
Added
- Pluggable UI factory — reactive, no-build, agent-editable.
src/server/ui-factory/template/(Preact + htmvia/vendor/*) withLAYOUT = {sidebar, THEME}andstyle.csstokens.POST /api/uis {"name":"my-ui"}orcp -r template ~/.cast/ui/<name>createshttp://host:1337/ui/<name>/andhttp://host:1337/<name>/(alsoGET /uilists).src/server/ui-registry.tsdiscovers~/.cast/ui/*every request,chokidarwatches~/.cast/ui→GET /api/uis/eventsSSEui_change→ template auto-reload. Built-in skillprompts/skills/ui-factory(andreferences/*) teaches the agent toread/write~/.cast/ui/*(files.ts:265blockssrc/server/public/dist/public). - Settings → Updates with quick check.
GET /api/system/version(currentfrompackage.json,fetchLatestVersion()with3srace,isReleaseInstall()) andPOST /api/system/upgrade(202+setImmediate runUpgrade).SettingsUpdates(settings-panels.js) fetches on open with4sclient abort, showsCurrent v…/Latest v…+Check/Update to v…(only whenisRelease && updateAvailable),dev — git pullhint otherwise. Lazy-loaded (settings-modal.jsonly visible tab).
Changed
- Base UI is now stable at
/app(and/cast,/default,/base,/based,/core,/main).server.ts:2264servesdist/publicfor all aliases +…/settingssub-paths,app.js:498viewFromPath()strips prefix,navigate()preserves it.GET /staysdefaultfor compat,GET /uilists factory UIs. Factory UIs also at/<name>/for convenience (http://host:1337/claude-ui/). - Install script opens firewall.
install.sh:109andinstall.ps1:92nowufw allow 1337/tcp(andfirewall-cmd/iptables/New-NetFirewallRulefallbacks) best-effort, socast server --publicis reachable without manualufw.
Fixed
- Factory UIs are isolated and live-reload while open. Previously
defaultat/could be overwritten by a factorywrite; nowsrc/core/tools/files.ts:265blockssrc/server/public/dist/publicandsrc/server/ui-factory/template,shared.ts:153expands~/, andserver.tsserves extra UIs only from~/.cast/ui/*withno-cacheHTML.
0.22.7
Fixed
- Web UI: sending no longer hangs on "Sending…". The composer awaited the full
waitForSessionStream(1.5s) +POST /chatbefore clearingsending, disabling the textarea and showingSending…for the whole round trip. It now clears optimistically, debounces the Send button for 400ms only, and restores the draft only ifsubmitMessageexplicitly returnsfalse(connection lost / upload failed).waitForSessionStreamtimeout is 400ms (was 1500ms) andtextareais never disabled bysending. - Web UI: streaming renders markdown progressively.
BlockViewstreamed plaintextNodeand onlyMessagerendered markdown atassistant_message— lists, code fences and bold popped at the end with a height jump. Streamingcontentblocks now useStreamingMarkdown(innerHTML = renderMarkdown(patched)) on every RAF, with virtually-closed fences (odd fences → +\n\```) so code blocks appear early and grow smoothly. Final height matches settled height. - Web UI: jump at streaming start removed.
chat.cssriseno longertranslateY(3px)— opacity only — and respectsprefers-reduced-motion. - Web UI: turn timer starts only on SSE
status:running. Previously it started on clientpendingSinceand jumped whenturnStartedAtarrived (or disappeared afterPOSTclearedpending). Nowelapsed-timer.jsignorespendingSinceand starts from serverturnStartedAt(anchoredDate.now()-turnStartedAt),Appno longer passespendingSince. - Web UI: API client no longer hangs forever.
api.jsnow usesAbortControllerwith 15s timeout (30s for session/history) and propagatesAbortErrorasRequest timed out. - Web UI: panel resize no longer leaks listeners.
use-panel-resize.jsnow handlespointercancel,setPointerCapture, and cleans up on unmount; both diff and sidebar handles are robust. - Web UI: sidebar search no longer races. Aborts previous
fetchviaAbortControllerinstead of only acancelledflag. - Web UI: composer UX. Restores draft only when
submitMessagereturnsfalse, keepstextareaenabled whilesending(placeholder no longerSending…), file input hasacceptfilter, images getloading="lazy"/alt,toolcards are keyboard-accessible, andpartitionFilesalso matches by extension.
0.22.6
Fixed
- Web UI: Send→Abort switches immediately, no disabled-button dead air. The
composer used to wait for the daemon's
status:runningSSE round trip before swapping the icon — for a beat the disabled Send button stayed on screen with the input already cleared, so it looked like the click did nothing.runningis now flipped optimistically the moment the optimistic pending message lands, and rolled back on the two early-return paths (SSE not open, POST failure), so the Abort button is visible while the request is actually in flight. The TUI was already correct. - Web UI: turn-timer no longer jumps when the daemon claims the turn. The
composer clock used to switch from a client-side
pendingSince(set at send time) to the server'sstartedAtthe instant the firststatus:runningSSE event landed — those live on different clocks, so the visible counter skipped forward or backward by tens-to-hundreds of ms each turn. The timer now captures the offset once on first server sighting and expressesstartedAtin client time thereafter, soDate.now() − startMsstays continuous through the handoff. The offset is cleared between turns and on reload-mid-turn falls back toDate.now(), so reconnects still seed the counter correctly from the server timestamp.
0.22.5
Changed
- Provider reasoning protocol is auto-detected, not hand-picked. Adding a
provider no longer asks you to pick a "Reasoning protocol" in the TUI — it's
detected from the URL (
openrouter,deepseek,qwen,minimax, …) and enriched per-model from models.dev, matching the web form. For proxy or aggregator endpoints the detector can't recognize, set it explicitly with/provider <name> reasoning <format>(orautoto reset to detection). /provider addjust saves the provider. It no longer forces the new provider active and runs the model/reasoning picker (which made adding a second provider a surprising one-shot). It now matches the web form: verify and save; activate and pick a model later via the Model tab or/provider <name>. The only exception is the very first provider with no active endpoint, which becomes the default so there's something to talk to.
0.22.4
Fixed
- TUI input died after
/reloadand MCP reconnect commands. Re-resolving skills/MCP/personas could leave Ink's stdin unref'd and its readable listener dropped, so keystrokes echoed below the composer instead of entering it — only a terminal resize recovered./reload,/mcp enable|disable|uninstall, and session switches now run a no-op suspension so Ink'sresumeInputreinstates stdin (the same path a resize takes). - No more
^[[6;1R/ screen-jump on/mcp. The suspension cancels the in-flight cursor-position query (whose reply echoed as visible garbage once raw mode dropped) and no longer clears the screen, which made it visibly jump.
Changed
- Composer lost its box border. The round frame was cosmetic and could
paint from a stale cursor position after resyncs, showing as a torn
╭──╭──line. It's replaced by a thin border-free ruled divider above the composer and above the status bar, and the prompt arrow sits at column 0. - Raw
console.logfrom core code no longer tears the TUI layout. Skill/ MCP warnings and a legacy-session warning were printed straight to stdout from code that also runs in the TUI process, breaking Ink's frame; they're now returned through the UI instead.
0.22.3
Added
/evolve— create reusable skills on demand. Replace the old proactive auto-suggestion (which proposed saving a skill after every multi-tool turn and felt aggressive) with an explicit command. Run/evolveat any point in a session: it analyzes the conversation plus the project's typical tasks and proposes reusable skills, which you pick in a multi-select picker (TUI / web). Chosen skills are written to the project's.cast/skills/. Nothing is created without your selection.
Changed
- Stable daemon token. The loopback credential the TUI uses is now the
same
serverToken(persisted in settings.json) instead of a fresh random token minted on every daemon start. Previously a restart (cast upgrade, redeploy) silently invalidated an already-running TUI, so the next authenticated call — e.g. the skill-save confirmation — failed with 401 until it reconnected.
Fixed
- Settings numeric fields (web UI). The turn safety cap on the Bash tab always showed 500 even after saving; numeric fields in Settings (Bash cap + Memory numbers) felt laggy and couldn't be fully erased — a cleared field snapped back to the current value. They now show real values, edit in one pass, and number steppers are visible again.
- Memory settings labels show the default (e.g. "(default 13000)") instead of duplicating the current value that the field already shows.
- TUI elapsed timer anchors to the daemon's start time (resumes across reconnect instead of resetting to zero) and ticks at 100ms.
0.22.2
Fixed
- Settings numeric fields (web UI). The turn safety cap on the Bash tab always showed 500 even after saving — it never read the real value, so a save looked like it never happened. Now it shows the actual cap. Numeric fields in Settings (Bash cap + all Memory numbers: dream/distill interval, prompt budget, search floor, checkpoint reserved) also felt laggy and couldn't be fully erased — a cleared field snapped back to the current value. Editing now works in one pass, and number steppers are visible again. The Settings modal also stops force-refreshing the Model tab on every save (which was part of the lag).
- TUI elapsed timer. In daemon mode the status-bar counter restarted from the client's local clock on every reconnect or reload mid-turn, resetting to zero instead of resuming. It now anchors to the daemon's authoritative start time (matching the web timer), and ticks at 100ms so the counter reads continuously rather than jumping between tenths.
0.22.1
Added
- Skill suggestions after reusable procedures. After a turn that used many
tools (4+) clearly following a repeatable workflow (cut a release, add a
component with tests, run the dev server and verify it), cast proposes
saving it as a project skill. The confirmation reuses the standard question
picker in both the TUI and the web UI — pick "Save as /name" to write
.cast/skills/<name>/SKILL.md, or "Dismiss". Conservative eval: trivial or one-off turns never suggest, an existing skill name is never overwritten, and declining once silences suggestions for the rest of the session.
Fixed
- Settings > Bash "turn safety cap" save. The save failed with a 400
because
/turn-capwasn't allowed in the settings command path that the settings modal uses — now it works from the UI as well as from chat. - A daemon crash after multi-step turns. The post-turn skill eval ran fire-and-forget; an unhandled rejection inside it (cloning the message array) silently killed the daemon on Node 22. The eval is now guarded and stores a compact transcript instead of the full message list.
- Skill suggestion reliability. The eval verdict is parsed robustly even when the model inlines its reasoning before the JSON (which used to read as "not reusable"), and the "(recommended)" marker was dropped from the save/dismiss confirmation.
0.22.0
Added
- Configurable per-turn iteration safety cap. The loop's runaway backstop
(a model that keeps calling tools forever could otherwise hang a turn) is
now a setting:
maxTurnIterationsinsettings.json, default 500 (range 10–10000), applied on the next agent call. Control it with/turn-cap [N|reset](TUI + web palette) or Settings → Bash./goal's own iteration budget still overrides it. Hitting the cap stops the turn with a warning; work done so far is never lost (persisted per tool batch).
0.21.0
Added
/goal— bounded autonomous "work until done" mode./goal <description>runs one turn that keeps iterating (tools → verify → fix) until the goal is met, without yielding for permission — at most one clarifying question if the goal is genuinely ambiguous. A leading number (/goal 10 fix the tests) or--steps Nsets the iteration budget (default 25 model calls; each call can batch several tools). Near the cap the model is nudged to wrap up, and if it burns through the run stops with a warning — it never loops forever. Verified live: the agent fixed a bug, hit and fixed a real Node-22node --test test/gotcha, ran the corrected tests, and reported honestly.
0.20.0
Added
/review— one word tells the agent to verify its own work: identify what changed (git diff / touched files), find and run the project's test and lint commands, and report honestly what was verified and what remains open. Works in the TUI and the web composer. The shared prompt carries the same honesty rule as the memory tools — the agent never claims a check it didn't actually run.
0.19.0
Added
- Live read-only share — a shared thread link (
/shared/<token>) now streams the agent working in real time: tokens, thinking, tool calls, and status, with a· livebadge. The visitor only ever receives display events (usage, steering, and plan state are filtered out) and there is no input surface, so the link is read-only by construction. On turn end the committed transcript refreshes. session_historytool gainsscope=global— the agent's "second brain". Previously it searched only the current project's sessions; nowscope=globalsearches across every project, so you can ask "when did we fix/decide X" and get the actual verbatim conversation, with the session title and date. Tool results now include that context.- Models are told to quote history verbatim and never fabricate. The
session_historyandmemorytool prompts now require quoting exactly what the search returned (numbers, paths, commit hashes) and admitting when a specific detail isn't in the results instead of inventing it. - Sandbox cleanup — deleting a sandbox session also removes its throwaway
folder (
~/.cast/sandbox/cast-<id>) on every delete path (web, TUI, ACP). Real project directories are never touched (exact-match only), and the web delete confirmation warns when the sandbox folder will go too.
Fixed
- Shared links never worked for anonymous visitors: the public page loads
app.js's full module graph, but only a hardcoded allowlist of static assets
was public — everyone else got
text/htmlfor the modules. Every real static file is now public (data stays behind the gated/api/*routes); page routes like/still bounce to/login. - Slow git operations no longer freeze the daemon:
git worktree add,worktree remove, marketplace clone/pull/update, and plugin install now run asynchronously, so a multi-second git op doesn't stall every other session's streaming. Fast probes (rev-parse, worktree list) stay sync deliberately.
Performance
- Static asset responses are cached in memory — one app.js hit went from ~182ms (re-hash + re-brotli per request) to ~4.5ms.
- Chat POSTs are acknowledged immediately instead of awaiting the turn's async setup (provider reconcile, hooks) — measured 402ms → 0ms.
- SQLite WAL is truncated periodically while idle so it can't grow unbounded (observed at 155MB) and every later checkpoint stays cheap.
- Larger V8 young generation for the streaming daemon (fewer minor GCs).
0.18.0
Added
- Web telemetry dashboard with its own dedicated
/dashboardroute (and/settingsas a route too, so both are navigable/bookmarkable and survive back/forward while the chat session stays mounted). Five tabs with 24h/7d/30d ranges:- LLM — requests, tokens, cost, cache rate, latency avg with p50/p95/p99, tokens/s throughput, and a paginated recent-requests table.
- Memory — memory-tool search calls, maintenance runs (dream/distill), entries stored, maintenance tokens.
- Performance — daemon API requests, latency percentiles, 5xx, per-endpoint table.
- Reliability — retries, retry rate, moderation blocks, error-type breakdown including harness-specific doom-loop and empty-response types.
- System — compactions, context use, file edits, and per-turn metrics (turns, tool calls/turn, tokens/turn, time/turn) plus tool usage with latency.
- Per-turn aggregation: every LLM completion and tool call is tagged with
the client message id (
turn_id), so one user request can be grouped into a single turn. Background maintenance (automatic dream/distill, checkpoint writer) is recorded askind = background. - Tool latency is measured per call (
tool_start→tool_end) and shown in the System tab's tool-usage table. - Settings and the dashboard close on Escape like every other modal.
Fixed
/shared/*deep links were silently broken:serveStaticreadreq.headers["accept-encoding"]unconditionally, so the synthetic{ url: "/" }request used by deep-link routes crashed into the catch block and 404'd. Optional-chaining fixes it (and unblocks the new/settingsand/dashboardroutes).
Internal
- Memory-maintenance pass timeout raised from 120s to 180s (matches the bash default) so a big project can consolidate without being cut short.
- New
docs/dashboard.md; the builtincastskill now documents its own TUI and web interfaces (references/tui.md, expandedreferences/web.md).
0.17.0
Added
- LLM error handling hardened. Moderation blocks are now surfaced as a clear "model refused the request" message instead of an empty
(no response)(OpenAIcontent_filter/refusalfield). Empty or reasoning-only replies are retried once with a doubled budget and a nudge telling the model to actually answer. OpenRouterstream_interrupted/PROVIDER_TIMEOUTandserver_errorcodes are retried instead of failing the turn. MiMo/MiniMax gateway moderation (421) and risk-control (441) blocks are relabeled with the real reason fromerror.param. - Context overflow on a 5xx is no longer retried blindly — it's routed straight to auto-compaction.
- Prompt-cache rate is visible everywhere — the TUI status bar and
/currentshow the% cachednext to tokens, plus a new Cost segment; the web status popover already showed it. - A memory reference was added to the builtin
castskill, so asking the agent about memory in chat produces concrete answers (storage,/memorycommands, how to enable/disable writing). - Memory settings UI reworked to match the rest of the modal: themed inline inputs with a check-to-save and reset-to-default buttons, and no trailing punctuation in item descriptions.
Fixed
- Vision fallback retry was discarded — a model rejecting images got images stripped and retried, but the successful retry was thrown away and the turn still failed with the original 400. The retry now completes normally (verified against a real non-vision model).
- Concurrent checkpoint writers on the same project now serialize per project instead of racing on the same
MEMORY.md(the second writer queued on the project memory lease could time out). - Multi-message runs (steering/follow-up) duplicated earlier turn content in the persisted partial on an abort; the accumulator now resets per stream attempt.
- Thin-client TUI message send no longer keeps a stale
[user · sending…]label on committed rows (Ink<Static>never re-renders them), and the resend-on-reconnect race is closed by claiming the message id synchronously on the daemon. - Marketplace seeding no longer blocks the event loop:
/plugin marketplace list|catalog(which the settings modal preloads in parallel with/memory) no longer triggers synchronous git clones on a fresh machine, which hung the Memory tab with "Loading…".
Internal
- Removed redundant per-message
sendingguards in both TUI and web thin clients now that the daemon dedupes byclientMessageIdsynchronously. - Retry classification widened (408,
stream_interrupted,PROVIDER_*,server_error, upstream wording);describeTurnErrormapsmodel_not_found,NO_PERMISSION, content-policy, andinvalid_api_keyto actionable messages.
0.16.0
Added
- Durable project memory now survives across sessions: project rules, architecture decisions, and cross-session facts live in plain markdown files under
~/.cast/memory/(MEMORY.md, sessioncheckpoint.md/notes.md/task progress, and a globalMEMORY.md), with a SQLite full-text index derived from them for fast search. - Checkpoint writers fire on context-window thresholds (default 4 × 20% up to 200K, 9 × 10% up to 500K, 18 × 5% above), clamped to the window minus a reserved safety buffer, so a fresh checkpoint exists when compaction needs to rebuild.
checkpointThresholds,checkpointReserved, and per-sectioncheckpointPushCapsare configurable and exposed in the TUI and web settings. - Memory search covers the whole memory tree — session checkpoints, notes, task progress, spillover files, and (with
memoryCcIndex) Claude Code memory underscope=cc. File-backed hits return a path plus snippet; a no-result search gives escalation guidance. - Rebuild context after compaction is injected as bounded sections (tasks ledger, session checkpoint, project/global memory, session notes) with explicit "resume directly" framing and a tail-aware reminder.
- Claude Code memory indexing (
memoryCcIndex) with frontmatter type parsing. - A complete beginner-friendly Memory guide in
docs/memory.md.
Fixed
- Checkpoint watermark is now an immutable message id, so compaction seq shifts can no longer invalidate it; a stale writer can't move the boundary backwards.
- Manual
/dreamand/distillnow use JSON-only prompts matching what the caller applies — the real-model consolidation actually removes stale facts and packages repeated workflows instead of silently doing nothing. - Checkpoint-writer sessions never self-compact, so a small-window run fails explicitly rather than derailing and advancing the watermark on empty state.
- Session-history search stays aligned after compaction (FTS rows now track seq shifts).
- Fork-mode checkpoint writers cover the latest turn and advance the watermark instead of stalling at the previous boundary.
Internal
- Memory is file-canonical: the SQLite index is a projection of the files, reconciled on search.
- Split dream/distill prompts into agent (file-tools) and JSON (non-agent) variants.
0.15.7
Fixed
- Disconnected clients no longer submit prompts. Web UI and TUI now gate sends on a live daemon/SSE connection, keep messages pending during outages, and retry with the same client id after reconnecting without creating duplicate turns.
Internal
- Tool tests use unique per-test workspaces. Parallel Vitest workers no longer remove one another's fixtures under
test/__test_tmp__/.
0.15.6
Fixed
- The Files pane stays current after agent writes. Directory refreshes now invalidate even while the panel is closed, bypass browser caches, and ignore out-of-order responses so a late stale response cannot replace a newer file tree.
- Large attachments cannot race message submission. Send stays disabled until every document upload finishes, and deferred upload failures cancel the message instead of sending a prompt the agent cannot access.
0.15.5
Fixed
- Concurrent turns are serialized reliably. TUI, Web UI, and separate Cast processes now claim a session before starting work, preventing duplicate agent loops, interleaved history, and stale cleanup from releasing a newer turn.
- Web message retries are idempotent across reconnects. Client message identifiers, pending-message recovery, and ordered SSE updates keep prompts visible without creating duplicate turns.
Internal
- Tests now run inside per-test environments. Each test gets an isolated home, cwd, settings file, SQLite database, and daemon state; custom database paths create their parent directories automatically.
0.15.4
Fixed
- The landing page removes redundant explanatory chrome. Install commands now have accessible copy buttons with the Web UI's project icon style, and the footer is reduced to quiet GitHub and MIT License links.
0.15.3
Fixed
- The landing workspace preview now keeps its top bar focused on connection state. Removed the repeated product and repository labels, matched the connected status dot to the Web UI, and removed the duplicate persona/build footer.
0.15.2
Fixed
- Landing and documentation chrome is cleaner and consistent on mobile. Removed redundant workspace labels, session/model status noise, and repeated persona cards; themed scrollbars now apply to the Pages document and its scrollable content areas.
0.15.1
Fixed
- GitHub Pages workspace preview now matches the actual Web UI structure. Removed the misleading local-first badge, fabricated capability metrics, and empty composer; the preview now shows the real header, sessions sidebar, chat state, and interactive persona switching.
0.15.0
Added
- The GitHub Pages landing page is now a Cast workspace overview. It presents the session flow, personas, capabilities, installation commands, and documentation in a responsive desktop/mobile layout.
- Documentation code blocks now have build-time syntax highlighting. Language labels, readable dark-theme tokens, and horizontal scrolling make shell, JSON, Markdown, and other examples easier to scan on any screen.
0.14.0
Added
- Settings now has a Personas browser. Personas are grouped by source, with the existing info popover for short descriptions and the book action for reading the full persona prompt.
0.13.37
Fixed
- Web thread pagination now explains its state at the top of the transcript. Older-message loads show a visible loader, failures offer retry, and reaching the beginning is labeled explicitly.
0.13.36
Fixed
- Release archives now contain only the native PTY runtime for the detected platform and architecture. Installers select Linux, macOS, and Windows x64/arm64 assets, while old releases remain installable through a safe legacy fallback.
0.13.35
Fixed
- Session lists no longer parse every assistant message to count tool calls. Tool-call metadata is stored in an indexed SQLite flag, including a migration for existing databases, reducing list queries as history grows.
0.13.34
Fixed
- Web messages no longer disappear during session startup or reconnects. Chat waits for the active SSE stream before dispatching, keeps unacknowledged messages visible for retry, and rehydrates accepted messages when the stream is delayed.
- Message retries are idempotent. A client message id prevents a lost HTTP response or reconnect from creating a duplicate turn.
0.13.33
Fixed
- Release archives include the native PTY runtime.
cast upgradenow installsnode-ptyand its native dependency alongside the bundle, so managed Bash background tasks work after a clean upgrade.
0.13.32
Added
- Long-running Bash commands can move to the background without a handoff. Interactive TUI and web sessions now run managed Bash tasks through PTY, automatically promote known server/watcher commands or foreground commands that exceed their grace period, and report the task id for progress and control.
Changed
- Updated Bash tool documentation and shared persona guidance for automatic background promotion, completion reminders,
bash_output, andbash_kill.
0.13.31
Fixed
- Provider list now disambiguates entries that share a base URL. When two saved providers pointed at the same host with different API keys, the web UI silently treated the first one as active (the save ✓ never disabled, the chat footer showed the wrong name, and
/provider listmarked both as active). Lookup now matches by name (falling back to URL + key), and the provider row in Settings shows a green "active" badge for the one in use.
0.13.30
Fixed
- TUI recovers across daemon upgrades and restarts. Stale daemon connections now time out, reconnect to the current daemon, retry the message, and restore the session state over SSE.
- Web chat no longer loses the first message during SSE startup. A draft waits briefly for its active session stream, with bounded recovery when the daemon is temporarily unavailable.
0.13.29
Fixed
- Provider, model, and reasoning changes now stay synchronized. TUI and Web UI selections apply atomically, cancel safely, normalize unsupported reasoning levels for the selected model, and persist across daemon turns and browser reloads.
- Cross-surface model changes now take effect immediately. A model or provider changed in another client is picked up on the next turn without stale model metadata or transport settings.
0.13.28
Fixed
- Provider and model selections now persist together. TUI and Web UI provider switches update the main model provider, repair stale provider settings, and safely fall back when an active provider is removed.
0.13.27
Fixed
- Daemon TUI now handles injected queue events. Follow-up and steer messages are added to the transcript and removed from the pending list when the daemon injects them, instead of remaining stuck as
Queued.
0.13.26
Fixed
- Follow-up handoff now waits for the runner's idle signal. TUI and daemon clients no longer depend on the timing of status or SSE events to start a queued request.
0.13.25
Fixed
- Local TUI follow-ups no longer remain stuck as
Queued. Messages that arrive while a turn is settling are picked up and run after the current response completes.
0.13.24
Fixed
cast upgradenow reports the final daemon state. After restarting a background daemon, the command verifies its new PID and identity, prints the running URL, and exits unsuccessfully if the replacement could not be confirmed.
0.13.23
Fixed
- Late daemon follow-ups now start reliably. A
/queueor Web UI follow-up that arrives while the previous turn is settling is handed to a new turn instead of remaining stuck asQueued.
0.13.22
Fixed
- Resource commands now use subcommand-aware daemon gating. Read-only
/mcp,/skills, and/sshinspection remains available during a turn, while operations that change tools, skills, or SSH configuration wait until the turn is idle.
0.13.21
Fixed
- Daemon follow-up messages no longer get stranded. Queue requests that arrive just after a turn finishes now start a new turn, so
/queueand the Web UI follow-up action remain reliable across the daemon API.
0.13.20
Fixed
- First-run daemon startup no longer fails without provider credentials. The daemon starts in setup mode and clearly directs users to configure a provider in the Web UI or use the terminal onboarding through
cast.
0.13.19
Fixed
- Daemon restarts no longer create empty sessions. The startup-only session used to select defaults is no longer persisted, preventing one empty SQLite record from accumulating for every daemon restart.
0.13.18
Added
- Stable API v1 for daemon integrations. Alternative clients can now use the versioned
/api/v1REST/SSE contract for session lifecycle, agent control, history, files, settings, and daemon metadata without depending on private web UI routes. - Published OpenAPI specification. Every daemon serves
/api/v1/openapi.json; GitHub Pages publishes the matching checked-in snapshot atopenapi/v1.jsonfor code generation and CI validation.
Fixed
- API input failures are explicit. Malformed JSON and invalid appearance, SSH, or provider-verification fields return actionable
400responses rather than leaking through as server errors.
0.13.17
Fixed
- Interrupted file searches stop immediately.
/abortnow cancels in-flightglobandgrepprocesses (fd/rg) and their built-in fallback scans instead of allowing a search to continue after the turn was cancelled. - Tool failures have a stable recovery contract. Every failed tool result now includes an error code, whether retrying is appropriate, and a suggested fix alongside its readable diagnostic. This is carried through the agent loop, SSE, and JSONL clients.
- Daemon abort recovery is covered end-to-end. A daemon session that aborts a stalled provider request reliably returns to idle and can start another turn.
0.13.16
Fixed
- Tool failures now tell the agent how to recover. Invalid arguments for filesystem, shell, SSH, search, and web tools are rejected before execution with the specific field and valid replacement range. Missing search paths are no longer reported as empty results.
- Destructive MCP calls now request write approval. The confirmation gate now recognizes Cast's real
mcp_<server>_<tool>names, including when hooks are enabled. - Failed background commands stay failed.
bash_outputnow preserves timeout and non-zero-exit errors instead of displaying anokstatus; bash and SSH also explicitly mark byte-truncated output. - MCP and cancellation errors have actionable context. MCP failures identify the server and tool, while interrupted web requests are labelled
[ABORTED]rather than as generic network failures.
0.13.15
Changed
cast webis the primary browser entry point again.cast serverremains a fully equivalent alias for daemon-oriented scripts and integrations, so either spelling manages the same daemon and sessions.
Fixed
- Daemon lifecycle is safe across restarts and upgrades. Cast now verifies the daemon's per-process identity before attaching to it or signalling its PID, so a stale state file cannot direct a client to — or stop — an unrelated process that reused the PID.
- Upgrades preserve a background daemon's address.
cast upgraderestarts a verified background daemon on its existing host and port only after it exits cleanly. Foreground daemons are left untouched for their owning terminal to restart manually, preventing an automatic upgrade from cutting off active work. - Long-lived daemons release inactive sessions without losing them. Idle sessions with no listeners and no active background task are unloaded after five minutes and rehydrated on demand; reconnecting clients recover authoritative pending state and report rejected control commands instead of silently losing them.
0.13.14
Fixed
- Opening a second
castwhile another was already running no longer strands the first on a dead daemon. Two concurrent launches used to race on the empty state file and each spawn its own daemon; the one that lost the race was never registered, and a TUI already pointed at it reported "Daemon unreachable" on its next message. Daemon startup is now serialized by an exclusive lock — concurrent launches wait and reuse the winner instead of stacking a second process. - An idle TUI now survives its daemon being replaced. If the daemon is stopped, crashes, or is restarted by an upgrade, the next message in the open TUI re-reads the daemon state, reconnects to the new daemon, and retries — the session (owned by the central store) continues instead of erroring.
0.13.13
Added
- Live agent events are now persisted for audit/debug. Tool runs, retries, doom-loop stops, compaction failures, errors, and turn ends land in a new
session_eventstable (readable viaGET /api/sessions/:id/events/history). These are execution telemetry, deliberately kept out of the conversation history so the model never sees them as context. - Versioned database migrations. Schema changes are now an ordered, tracked migration list (
schema_migrationstable) applied once each inside a transaction — the standard Flyway-style pattern — replacing the previous ad-hoc column checks. Existing databases migrate in place without losing any sessions or messages.
0.13.12
Fixed
- Stale
cast webno longer becomes a phantom message. Upgrading from a pre-0.13.11 install (cast upgradefrom 0.13.9 or older) used to restart the daemon viacast web start --port 0. Since the command was renamed tocast server, the new binary readweb start --port 0as a TUI prompt instead of a subcommand — a phantom[user] web start --port 0message landed in a fresh session.cast webnow prints a clear "renamed to cast server" error instead of falling through to the prompt.
0.13.11
Changed
cast webis nowcast server. The daemon was the single-writer backend every surface talks to, not just the web UI, so the name was misleading. The command, state file (server.json, auto-migrated fromweb.json), env vars (CAST_SERVER_*), and the settings key (serverToken, auto-migrated fromwebPassword) are allservernow. The web UI is unchanged — it's just served by thecast serverdaemon.cast webis no longer accepted.- The daemon is persistent and single-instance. It stays up after the TUI exits so background processes keep running and the web UI stays reachable; repeated
cast/npm startreuse the one running instance instead of stacking orphaned processes. cast runandcast run --interactivenow go through the same server daemon as the TUI and web UI. Sessions land in the shared store,-c/-sresume works, and JSONL commands (including/worktree, plan review, question answers) round-trip through the server's command surface.
Added
Ctrl+Lclears the composer in any state; idleEscno longer wipes typed text (a stray press used to delete your message).- E2E harness for the server paths:
npm run e2e:jsonl(plan mode through the daemon) andnpm run e2e:hooks(hooks firing on the daemon).
Fixed
- Stopping a running turn now needs a deliberate double-
Esc; a single strayEscno longer kills a long in-flight turn. - The ASCII banner no longer disappears ~0.5 s after startup (a settle resync cleared it).
- Exiting is clean: no
^[[15;1R-style cursor-query garbage in the shell, and the last TUI frame is cleared. - The question picker names exactly which option is missing its required
value, so the model knows what to fix. - Older session history loads a page at a time (
/olderorPageUp) instead of flooding the terminal scrollback on resume.
0.13.10
Added
- Older session history is paged instead of dumped. Resuming a long session no longer floods the terminal scrollback with the whole transcript (which pushed the viewport past the buffer limit and made the start unreachable). Only the most recent turns load;
/olderorPageUploads more on demand. Ctrl+Lclears the composer in any state. IdleEscused to wipe typed text — a stray press deleted your message. Clearing now lives onCtrl+Lonly, andEscis reserved for stopping a running turn.
Changed
- Stopping a turn needs a deliberate double-
Esc. The first press shows[Press Esc again to stop the turn]; a second within 2 s aborts. A single strayEscno longer kills a long in-flight turn.
Fixed
- The banner no longer disappears after startup.
settleResync(added in 0.13.7) cleared the screen ~0.5 s after mount, erasing the ASCII banner and composer frame because the banner lives outside Ink's tree. Light resyncs now reprint it. - Clean exit: no terminal garbage after quitting. A pending cursor-position query (
\x1b[6n) could have its reply echoed into the shell once raw mode dropped (^[[15;1Retc.). Exiting now cancels the query, stops polling, and clears the screen. - The question picker reports exactly what's wrong. When a model omits an option's required
valuefield, the error now names the offending option instead of a generic count message.
0.13.9
Fixed
- Text-only models stop receiving image attachments. When a provider rejects
image_urlmessage parts (e.g. deepseek-v4-pro on a text-only endpoint), the rejected image messages are now removed from the session — previously they stayed and every turn re-sent them, failing again. Models that don't support images now just see the file path.
0.13.8
Fixed
- Raw mode is re-asserted if the terminal drops it. Some terminals / SSH wrappers quietly restore echo and line buffering, which made typed text appear below the composer frame instead of entering it. A 2 s watchdog re-enables raw mode whenever it's detected as off.
0.13.7
Fixed
- Resumed sessions re-settle the layout. Opening an existing session replays its history, which could scroll the composer up mid-screen (input landing below the frame) with no streaming turn to trigger the usual repair. A light resync now runs shortly after mount to reposition the composer at the bottom.
0.13.6
Fixed
- A stacked display (composer rendered above the input) is now corrected. When DECXCPR scroll detection was unavailable, the cleanup resync never fired and the mis-rendered layout persisted; the resync now proceeds even without scroll detection, so the display self-heals.
0.13.5
Fixed
cast upgradenow restarts a running daemon. Previously it replaced the binary but a livecast webdaemon kept executing the old bundle until manually restarted, so fixes never reached the daemon process. If a daemon is running at upgrade time it's now stopped and restarted on the new build.
0.13.4
Fixed
- DECXCPR scroll-polling stops if the terminal never answers. A terminal that echoes the query instead of delivering it (raw mode lost, e.g. on resume) used to keep re-echoing garbage on every poll — now the poll gives up after the first unanswered query, so the flood can't recur even on a hostile terminal.
0.13.3
Fixed
- Composer input no longer goes dead on terminals that answer the DECXCPR scroll-poll slowly. The poll attached a temporary stdin listener per query, which swallowed keystrokes for up to 400 ms a time; the response is now detected through the composer's own input pipeline, and the poll skips terminals that aren't actually in raw mode.
0.13.2
Fixed
- DECXCPR scroll-poll responses no longer leak into the composer. The terminal's cursor-position replies (echoed while stdin drops out of raw mode, e.g. during a bash tool) used to land as visible garbage like
^[[67;1Ror68;1Rin the input buffer. In-flight queries are now cancelled the moment the terminal suspends, and stray response remnants are dropped defensively.
Internal
turn-runner-statetests now write their sentinel files behind a fake HOME, so a killed test run can never pollute the real~/.cast/sessions/.
0.13.1
Fixed
- Abort is always responsive. Esc cancels a provider retry backoff immediately (no more waiting out a 30 s sleep); the OpenAI SDK's own uninterruptible retries are disabled (
maxRetries: 0); a parallel tool batch closes after a 2 s grace even when a tool (e.g. a hung MCP server) ignores the abort signal — the turn always lands on "aborted". - Turn errors stay in the transcript. A 4xx failure is committed to the chat history instead of sticking above the composer until the next turn.
- Question picker and plan/build mode work in the daemon-mode TUI. The question picker opens and answers go back over HTTP;
/plan,/build, and the plan-approval dialog sync the mode to thecast webdaemon so the next turn actually runs in the chosen mode. - Provider/model switches take effect without a restart. A running
cast webdaemon picks up settings.json changes (made in the TUI or by hand) at the next turn, and reconciles a stale session model against the new provider's model list. - Image files attach inline. Pasting (Ctrl+V) or attaching (Ctrl+G) an image path sends the image to the model as a real attachment instead of a bare path it must
read. - Provider retries surface in the web UI as a
[Retrying (attempt N)…]row instead of a silent spinner. - Undo checkpoints and subagent transcripts are persisted to the session database, so
/undohistory and subagent work survive a daemon restart.
Added
- ACP permission flow via the SDK.
requestPermissionViaBridgenow sends a typedsession/request_permissionrequest to the client (withPromise.racetimeout of 60 s), instead of emitting a customrequest_permissionnotification that the client couldn't reply to. The reply is the typedRequestPermissionResponse—outcome.outcome === "selected"withoptionId === "allow_once"grants, anything else denies. - Plan-mode pickers.
onPendingStateChangewired throughcreatePlanState— plan questions now surface asrequest_questionnotifications, plan transitions asrequest_plan_approval. Two custom extension methods (answer_question,plan_review) accept replies and resolve the pending state. tool_call.kindmapped to ACP constants:bash→execute,read→read,write/edit/patch→edit,grep/glob/web_fetch/web_search→search. Previously every tool reported its own name as the kind.available_commands_updatesent once on session creation with the full slash-command list (SLASH_COMMANDSfromsrc/ui/commands.ts). Names are stripped of leading/to match ACP conventions;input.hintis set on commands that take arguments.usage_updatewith full payload. Now sends{ used, size, cost: { amount, currency } }per ACP spec —usedis the current turn's token count,sizeis the model's context window,costis the cumulative session cost in USD summed across allusageevents.- Multi-modal content in
session/prompt. ACP v1PromptRequest.prompt[]accepts text, image, audio, and resource blocks.textis forwarded as-is,image(base64 + mimeType) is converted to animage_urldata URL and passed through cast's existing vision path (runAgentLoopalready strips unsupported image_url parts — seeloop.ts:1173). Audio and resource blocks are dropped with a marker note (cast has no audio/embedded-resource ingestion path through ACP yet). session/loadandsession/resumereplay history. When a client opens an existing session, the bridge re-emits every persisted user/assistant message asuser_message_chunk/agent_message_chunknotifications in chronological order, so the editor sees the conversation history. Replay is fire-and-forget — thesession/loadresponse is returned immediately.
Changed
- ACP bridge migrated to
@agentclientprotocol/sdk. Thecast acpwire transport is now powered by the official SDK (v1.3.0, zero dependencies, Apache-2.0) — all JSON-RPC serialization, schema validation, and protocol negotiations are handled by the library. The hand-rolledrpc.ts/types.ts/handler.ts/tools.ts/index.ts(≈600 lines) have been replaced byagent.ts(the typedacp.agent({ name: "cast" })factory + handler registration) andbridge.ts(the cast-side adapter that translatesAgentEventinto SDKsessionUpdatenotifications and routes SDK requests intorunAgentLoop). Method names now use the slash-separated protocol convention (session/new,session/load,session/prompt,session/set_mode,session/cancel,session/close,session/resume,session/list,authenticate). The oldtools/list,permission/grant,permission/deny,answer_question, andplan_reviewmethods have been dropped —tools/listwas never an ACP spec method, and the plan/picker bridge (request_question,request_plan_approval) is deferred to a future iteration. - Expanded agent capabilities.
agentCapabilitiesnow advertisespromptCapabilities: { audio: false, embeddedContext: true, image: true },mcpCapabilities: { http: false, sse: false },sessionCapabilities: { close: {}, fork: {}, list: {}, resume: {} }— matching the structured shape editors expect instead of the previous flat boolean set. - New ACP methods.
authenticate(returns empty{}),session/list,session/close, andsession/resumeare now registered on the SDK agent.session/listreads from the SQLite session database and returns{ sessionId, cwd, title }.session/closedeletes the session and aborts its runner.
0.13.0
Added
- Single-writer daemon architecture:
cast webis now the one process that ownsrunAgentLoopand is the only writer to the SQLite session store. The TUI (cast, no subcommand) is a thin client of it over HTTP + SSE instead of running the loop locally — on launch it auto-spawns the daemon on loopback (reusing one if already running) and renders from the same/api/sessions/:id/eventsstream the browser uses. A session opened in both the TUI and the browser now streams live (tokens, tool calls, status) to both surfaces, andabort/steerfrom either stops or redirects the turn for both.cast web stopnow also disconnects the TUI's SSE stream. SetCAST_NO_DAEMON=1to keep the TUI on the previous local-loop path (CI/headless).
0.12.28
Fixed
- Web sidebar: search showed "No sessions match
" on every non-empty search, hiding the matches above it. The empty-state check compared the wrong list (the full session list, which is empty when a search is active) instead of the filtered list. Now the banner only appears when the filter actually returns nothing. - Web Changes tab / Files tree: external file edits in the session cwd (IDE save, CI hook,
touch, etc) now refresh the diff and the file tree in real-time. Previously the only source of refresh was the next agent tool_end. A non-recursivechokidarwatcher on the cwd root fires anfs_changeSSE event after a 500ms debounce while the session is idle, gated so it never races a running turn. Top-level and subdir paths both work;.git,node_modules,dist,build, and other noise directories are ignored. The nativefs.watchpath had an inotifymax_user_watchesceiling that silently killed the watcher on real-world cwds — chokidar's pooling avoids it. - Web sidebar footer: showed "No model selected" for a frame between mount and the
/api/configresponse arriving. The sidebar now renders "Loading…" until the model has actually been fetched.
0.12.27
Added
- TUI:
/reasoning-displaycommand to hide reasoning blocks by default. The setting is persisted tosettings.jsonand survives restarts. - Web: reasoning-display toggle in Settings > Appearance, synced with
settings.json.
Fixed
- Web streaming reasoning block: the 1200-character cap from v0.12.21–v0.12.25 is restored. An earlier v0.12.26 commit had removed it, which let long reasoning streams disable the TUI scroll guard and produce occasional "jumps" in the visible content. The cap keeps the cursor-below-viewport invariant that
useTerminalResyncdepends on.
Internal
- Several experimental TUI reasoning-display tweaks that landed on
masterafter v0.12.26 (1-row live preview redesign,\nflattening in live/settled history, hidden-think-block clamp accounting) have been reverted. They never shipped in a release.
0.12.26
Fixed
- Sidebar / picker row listing was slow on big DBs —
listSessionSummariesandsearchSessionSummariespreviously built each session's summary by SELECTing every user/assistantcontent_jsonrow, JSON.parsing the whole conversation, and counting turns in JS. On a 218-session DB that meant 9 MB of allocations and 9000+ JSON.parse calls per listing. The new path aggregates in SQL via covering indexes: user count and assistant count viaidx_messages_role, the with-tool-calls slice via PRIMARY KEY + JSON filter then subtracted to preserve the old "exclude intermediate tool-call-only steps" semantic, and the first user message via aMIN(seq)JOIN on the primary key. Field-level semantics of the row'smsgCountandfirstUserMessageare unchanged. Measured on 57.131.129.41 with 218 sessions: TTFB onGET /api/sessionsdropped from 448 ms to 138-163 ms (3x faster).
0.12.25
Fixed
- 400 context window exceeded now auto-recovers from the agent loop instead of killing the turn. When the LLM rejects a turn with a context-overflow, the loop now drops the largest tool result in history (any
read/grep/web_fetchwhose output is already anchored in the conversation and was being re-sent on every retry), replaces it with a short placeholder that names thetool_call_id, the size that was dropped, and how to re-fetch with a narrower scope, and appends a<system-reminder>so the model re-issues the call withoffset/limitinstead of asking for the same content again. The new path runs once per turn; if the in-place shrink wasn't enough the existing LLM-based compaction path tries next, so the user only sees a raw error after both options have failed.tool_call_idis preserved across the swap so the conversation stays wire-valid for the next retry.
0.12.24
Fixed
- TUI streaming flicker on every token:
ThinkBlockParser.parseContent/flushreturned the entire accumulated buffer on each chunk, but theStreamChunkcontract is delta-only — downstream doescontent += chunk.content. The cumulative return made each per-token redraw re-render every prior line, so the assistant visibly typed "line by line" with each previous line reprinted before the next arrived. The parser now tracks anemittedBufferLenoffset and returns only what's new since the last yield, slicing the underlying buffer with[emittedBufferLen, …]. The buffer is compacted only when the already-emitted prefix grows to a meaningful fraction of the total length (keeps amortised cost flat on long streams without shifting the offset). Covers both the think-block and content branches and the trailing-tailflush. Web UI streams ride the same parser, so the web client picks up the fix for free. Regression test added intest/vendors.test.ts.
Added
- Web UI file preview: copy-to-clipboard button next to the existing download icon. For text/table previews it copies the rendered content once it has loaded (disabled until then); for image/PDF it copies the preview URL. Brief check-mark confirmation via the existing
icons.checkfor ~1.5s. Falls back to a hiddentextarea+document.execCommand("copy")if the modern Clipboard API isn't available, and swallows denied-permission errors silently so the modal stays usable.
0.12.23
Added
- Web UI:
cast web statusis now reachable from inside the running browser session — new "Server" tab in Settings shows whether the daemon is running, its pid, host:port (with a "reachable from other machines" note when bound to0.0.0.0), start time and uptime. Same info as the CLI command; the panel calls the newGET /api/web/statusendpoint, which reads the daemon state file viareadLiveWebStateso a stale entry (process gone) is auto-cleaned on read.
Fixed
- TUI: a long reasoning stream (
SPLIT_REASONING_CHARS = 1200) could render as N[reasoning]sections instead of one.appendTextBlockforcedcontinued: falseon the split-off chunk and on the previous block when a different kind took over, so the[reasoning]prefix in ChatLog rendered again on every mid-run chunk and on the tail at the kind boundary. The split-off chunk now inherits the source block'scontinuedflag (block.continued ?? false) and the kind-boundary settle now preserveslast.continued ?? falseinstead of forcingfalse— so only the very first chunk of the run carries the prefix, every later chunk is a silent continuation. Data is unchanged (still no loss across splits); the active tail stays bounded by the scroll-guard cap.stream-blocks.jsis browser-neutral, so the same fix tightens the web UI's collapsed reasoning output for free.
0.12.22
Fixed
- Web UI: clicking a thread closed the sidebar immediately and highlighted only the newly-selected row, instead of waiting for the
/api/sessions/:idfetch to land and glowing both old and new at once.setSidebarOpen(false)now runs at the start ofselectSession(right aftersetSelectingId) so the drawer collapses instantly on big-thread clicks that take a second or two to load. The chat area's "Loading…" empty-state already takes over during the same window, so the user sees a clear "switching" state instead of a stale list with a mute click. The dual-highlight bug —isActive = s.id === activeId || selectingmatched both the previous active row AND the new selecting one during the transition — fixed by switching toisActive = selecting || (s.id === activeId && !selectingId)(addedselectingIdas a prop so the formula can tell "this row is the picker target" from "some other row is the picker target"). Bootstrap / popstate paths don't go throughselectSession, so their old "wait for response" timing is preserved.
0.12.21
Fixed
- TUI: long reasoning streams could trigger a rare scroll jump when the user scrolled up mid-stream. A single still-streaming
thinkingblock grew unbounded and pushed the live region past the viewport, which disableduseTerminalResync's DECXCPR cursor poll (the natural cursor-below-viewport position looks identical to a user scroll). With the poll off, Ink'sCUU + eraseredraws landed at the wrong rows on user-initiated scroll and the visible content jumped.appendTextBlocknow caps the active reasoning block at 1200 chars (SPLIT_REASONING_CHARS) — the older portion moves into a settled (continued: false) sibling that drains to<Static>, the active block keeps the tail. The live region stays within the viewport, the poll keeps running, and the scroll guard works as intended. Content and tool blocks are unaffected (content already drains viasplitCompleteLines, tools stay compact).
0.12.20
Fixed
- First-run reasoning picker for MiniMax:
buildReasoningParamsfor format"minimax"funneled every non-adaptive, non-disabledvalue (including"off"and stale levels like"low"/"medium"/"high"/"max"that can land here from a savedreasoningLevelset against a different provider) into{ reasoning_split: true }withenabled: true. For"off"that meant picking "off" was a no-op — the server's always-on default still ran reasoning. Reorder the switch sooff/disabledmap tothinking: { type: "disabled" }withenabled: false(verified live against api.minimax.io),adaptivestays its own branch, and the always-on fallback serves only true "on" levels. Hotfix on the reasoning dialect ladder that landed in 0.9.10.
0.12.19
Added
/undocommand: rolls back the last turn — restores files from the turn's checkpoint (shadow copies of files the agent touched) and drops the last user message and everything after it. Refused while the agent is running (use/abortfirst). No-op when there's no checkpoint (very first turn, or/clearalready rolled the session back). Web UI shows a[Undone: ...]notice when triggered from the client./undorequiressrc/core/checkpoint.ts— shadow file storage + a marker on the parent commit for any new files added during the turn, restored on demand.
Fixed
- Web UI marketplace install: button no longer reappears after a successful install. The panel was reading
data.pluginsfrom its own props (always undefined — parent passed onlydata.marketplace), so the installed-plugin check silently failed and the row kept showing "Install" after install. Now the parent passes the installed list as a separate prop and refreshes it after every/plugincommand. - Web UI marketplace install feedback: per-row pending state with a spinner mid-flight plus a brief
installed ✓label on success. The modal's globalbusyflag only flashed for ~100ms, which read as a no-op click. - Web UI marketplace block: stray horizontal scrollbar suppressed.
.plugin-catalog-listhadoverflow-y: auto, which implicitly makes the x-axisautotoo; long descriptions triggered a horizontal scroll where only vertical should exist. Explicitoverflow-x: hiddenkeeps vertical scroll, kills horizontal.
Internal
- Biome warnings on new
/undoand worktree code: removed unused imports (copyFileSync,realpathSync,sep,samePathincheckpoint.ts;createCheckpointincommands.ts;AppConfigtype inbridge.ts); hoisted a hot-path regex literal incommands.tsto a module-level constant.
0.12.18
Fixed
- Web UI settings modal layout on mobile: form fields stack vertically instead of cramming 3 inputs into one narrow row; select dropdowns (Web tab search/fetch) are full-width instead of clipped at 180px; marketplace plugin names truncate with ellipsis instead of overflowing the card with horizontal scroll.
- Web UI settings dom warnings: password inputs wrapped in
<form>elements; API key field givenautocomplete="off". - Web UI model pickers streamlined: removed "(reasoning)" suffix from model names, removed "— @ provider" tags from section titles, sub-agent and plan-mode provider pickers now show "openrouter (default)" instead of duplicating the active provider in the dropdown list.
- Web UI composer: placeholder simplified to "Type a message…", max-height raised to 150px to prevent scrollbar on mobile.
Changed
- Web UI settings reasoning section: "Reasoning — current: off" simplified to just "Reasoning".
0.12.17
Fixed
- Web UI composer placeholder simplified to just "Type a message…" — the previous context-sensitive hints ("↑↓ to navigate, Enter to pick" / "Type your answer…") were truncated on narrow mobile screens and added no value since the picker UI is self-explanatory.
- Web UI composer textarea no longer shows a scrollbar on mobile when the text wraps past two lines.
max-heightraised from 100px to 150px, andoverflow: hiddenremoved (it was blocking the JS auto-resize on iOS).
Internal
- 69 inline regex literals hoisted to module-level
constdeclarations acrosssrc/(biomeperformance/useTopLevelRegex), including all core modules (frontmatter,hooks,mcp,plan,plugins,rules,session,skills,tools/,llm,vendors,startup,upgrade), all UI modules (commands,keys,stdin-buffer,input-parser,word-nav,useTerminalResync,App), and all web modules (app,composer,message-submit,reasoning-split,sidebar-utils,tool-card,new-session-modal,server,bridge,commands). - 16
noAwaitInLoopswarnings suppressed with explicit engineering justification — all are genuinely sequential operations wherePromise.allis semantically wrong or unsafe: the main agent turn loop, SSE streaming, MCP cursor pagination, provider probe retry, filesystem traversal (EMFILE risk), sequential file upload, and interactive SSH key/picker prompts. - Removed one unused
GITHUB_URL_PREFIXconst inbridge.ts. text-replace.tsregex consts repaired after a brokenreplaceAllin an intermediate commit self-referenced one const and left three others unused. Tests back to 1508/1508.
0.12.16
Added
/worktree <name>slash command — switch the running session into an isolated git worktree from inside the TUI. Behaves identically to the existing--worktree <name>CLI flag: creates the worktree at<repo>/.cast/worktrees/<name>on a freshcast-<name>branch offHEAD(or reuses an existing one), switchessession.cwdso subsequent bash/read/write/edit see the worktree path, and leaves the main checkout untouched. A newstateevent'scwdfield reflects the change so headless consumers can observe it. Branches offfindCanonicalGitRoot, so/worktree nestedinvoked from inside a worktree still anchors the new one at the main repo's.cast/worktrees/dir — no linked-worktree-of-a-worktree.cast run --interactive(JSONL protocol) now accepts acommandaction that pipes a slash command through the samehandleInputthe TUI uses, so evaluators and headless agents can drive/worktree(and any other slash command) end-to-end without a real TTY.{type:"command",name:"worktree",args:" foo"}is the canonical shape.noticeevents forward transient toasts;stateevents carrycwd = session.cwd ?? result.cwdso cwd changes are observable.--worktree <name>/-w <name>(also oncast run) runs the session inside an isolated git worktree. The worktree is created (or reused) at<repo>/.cast/worktrees/<name>on a freshcast-<name>branch offHEAD; bash, read, write, and edit all see the worktree path, and the main checkout is left untouched. Two--worktreeruns in parallel never collide. Add.cast/to your.gitignore. The worktree and branch are left on disk on exit — remove them withgit worktree remove .cast/worktrees/<name>andgit branch -D cast-<name>when done. Requires being inside a git checkout with at least one commit; both--worktree fooand--worktree=fooare accepted.
Changed
cast -c/cast --continueis now scoped to the current working directory. Previously it resumed the most recent session globally (whatever project was last touched on the machine), which silently dragged the agent into an unrelated project when the usercd'd somewhere fresh. It now resumes the most recent session in the current cwd and exits 1 with a clear error if no session exists there. The "no session" check runs before provider-model fetch and MCP setup, so the failure path is fast (milliseconds) instead of waiting on a 15-second model probe first.--resume=<id>still works globally — the lookup by explicit id is not cwd-bound, since the user already knows which session they want. Mirrorsclaude -c's behaviour (an upstream tracker for the same bug is anthropics/claude-code#35226).- Web UI:
preact,preact/hooks, andhtmare now served from/vendor/*.mjsinstead ofhttps://esm.sh/.... On a remote network with non-trivial RTT to esm.sh this removes 3 round-trips from the critical render path (~900ms saved on a 100ms-RTT connection, where each preact fetch takes ~400ms); on localhost the improvement is invisible because the round-trips are sub-10ms. CSP also dropshttps://esm.shfromscript-srcandconnect-srcsince no code path needs it anymore. The google-fonts preconnect hints are downgraded todns-prefetchsince the only font load is user-initiated (Settings > Font) and the DNS resolution cost on every page load was wasted for the 99% of loads that never switch. - Web UI composer submit no longer refetches the whole session list after sending a message. The previous
loadSessions()round trip was redundant — the server pushessession_updateSSE events with the new message count and auto-derived title after every turn — and it stalled the optimistic feedback loop so the user saw nothing change in the sidebar until the round trip landed.
0.12.15
Added
- Web Settings → Model tab now exposes a 3-state
thinkingcontrol for MiniMax-M3 (Enabled / Adaptive / Disabled) instead of a single always-on toggle. The wire format matches the live OpenAI-compatible API: Enabled is the default (no field), Adaptive sendsthinking: { type: "adaptive" }, Disabled sendsthinking: { type: "disabled" }. The Web UI now surfaces the saved level in the section header (previously hard-coded to "off" because the web/currentendpoint was not returning the field).
Fixed
- Web Settings → Provider tab no longer freezes the modal:
SettingsProviderwas callinguseRefwithout importing it, so the click on the tab threwReferenceErrorduring render and Preact dropped the pane subtree. The picker now renders the saved providers, edit/delete buttons, and the add form. - Web Settings → Model tab reasoning picker mirrors the saved level after apply. The dropdown previously snapped back to "Pick a level…" after a successful apply, which made the selection look lost; it now stays on the just-applied value and the ✓ button returns to its gray state when the picked value matches the current one.
0.12.14
Fixed
- Web UI theme picker now wraps long theme names inside the swatch button so the full label is visible instead of overflowing the card.
- Open-work gate no longer leaks the user-facing "falling through to the user" notice into the model transcript: it is delivered only through the
open_work_gate_exhaustedevent, so a resumed session does not re-read the orphan reminder.
0.12.13
Fixed
- TUI question picker now forwards the user's free-form answer to the model instead of a literal
(custom — see above)placeholder, and the web bridge accepts any value (not just model-supplied options), so a custom answer typed in the composer no longer gets dropped. - Web question card now shows an inline textarea under each option group so the user can type a custom answer alongside the model-supplied options; the textarea is styled to match the option buttons.
0.12.12
Fixed
- Preserved Web UI tool card state (expanded result, open image preview) across the rest of the turn: a tool the user opened no longer collapses when another tool's
tool_start/tool_endevent arrives, and stays open through the finalassistant_messageswap from the live stream into the settled message. - Added
Content-Encoding: gzipfor/api/sessions/:idand other JSON responses above 8KB; large agentic threads open noticeably faster on slow links (the/api/sessions/:idpayload typically drops 5-10x with the repetitive tool output these threads contain). - Added a
(session_id, role, seq)index on themessagestable to backgetHistoryPage's boundary lookup and existence check.
0.12.11
Fixed
- Preserved in-progress Web UI reasoning and tool blocks across page reloads, including deterministic ordering while a tool is running.
- Reconciled active streaming snapshots with persisted assistant messages to prevent duplicated or reordered reasoning and tool cards after reconnects.
0.12.10
Fixed
- Fixed TUI and Web UI question pickers so multi-question flows do not reopen duplicate pickers or remain visible after answering.
- Fixed plan approval options to render vertically and removed duplicate plan decision notices.
- Fixed TUI plan mode so
plan_doneremains available after switching from Build mode, including providers that require explicit tool parameters. - Added provider request timeouts and a bounded LLM retry deadline so Settings and stalled provider calls no longer remain in loading indefinitely.
- Updated question answer summaries to use the compact
Question: … Answer: …format.
Changed
- Split token scoreboard metrics into input/output percentile columns and removed obsolete Commit and Consistent columns.
0.12.9
Fixed
- Fixed Web UI startup after the recent client modularization by restoring the resource-load and modal-focus hooks used by the root application.
Changed
- Split Web UI settings panels, message submission, panel resizing, and SSE handling into focused modules without changing the TUI or Web UI behavior.
0.12.8
Added
- Added a themed web sign-in page with HttpOnly, SameSite session cookies, SQLite-backed web sessions, disabled API caching, and failed-login rate limiting.
- Added provider-aware reasoning configuration documentation and built-in references for providers, web access, and project configuration.
Fixed
- Project-local hooks now participate in the project trust prompt even when
.cast/hooks.jsonis the only local resource. - Existing sessions now derive missing sidebar titles from their first user message, while explicit blank titles remain unchanged.
- Updated plan, MCP, hooks, skills.sh, tool, and eval documentation to match the current runtime and CLI behavior.
Changed
- Simplified the shared Cast prompt so configuration guidance has one source of truth in the built-in
castskill. - Removed the obsolete prompt-secrecy rule from the open-project harness discipline.
0.12.7
Added
- Added a behavior evaluation bench covering planning, tool use, task execution, and other core interaction contracts, with a certification "Model Scoreboard" published on the docs site (
--scoreboardon the eval runner records each model's per-case pass rate across at least 3 attempts per case).
Fixed
- Completed web planning transitions so plan decisions consistently reach the intended next state.
grep: aligned glob paths across the ripgrep-backed and built-in fallback implementations, and fixed the fallback failing to match when the search path names a single file instead of a directory.- Rendered escaped Unicode in tool results as readable text.
- Stabilized web settings and session interactions.
- Plan mode's read-only command check rejected safe commands like
ls -la 2>&1orcmd >/dev/null 2>&1— fd-duplication and null-device redirects were caught by the blanket output-redirection guard even though neither writes a persistent file. bash: background tasks (run_in_background: true) no longer get killed by a default timeout — they're open-ended by default (dev servers, long builds) and only get a kill timer when the model explicitly passestimeout. Atimeoutof0or negative is treated as "no timeout" rather than firing almost immediately./continueand starting a new session now restore the full session state (reasoning settings, turn metadata, title, pinned flag, todos, share token) instead of a partial subset, and reset SSH host resolution and provider/model validation consistently when switching sessions or projects.
Internal
- Added plan, task, and skill judgment cases plus single-tool core contract coverage; eval cases now support an isolated per-case working directory so a case's synthetic scenario can't be second-guessed against the real repo.
- Fixed a fixture race condition in the eval runner's
--repeatmode where concurrent attempts of the same case corrupted each other's on-disk fixture state, and wired--baselineregression comparison into--repeatruns (previously single-run only). - Synchronized evaluation and tool documentation with the current behavior bench and background Bash behavior.
0.12.5
Fixed
- Planning flow: complex build-mode tasks now consistently request planning before implementation, while an explicit user choice to continue in Build is preserved across subsequent turns.
- Web UI: plan-mode entry and plan approval now use the same choices as the terminal UI, including refine, immediate implementation, fresh-context implementation, and manual Build handoff.
- Web UI: every plan-mode decision is saved as a persistent system card in the chat history, so the selected path remains clear after reload.
0.12.4
Fixed
- Web UI: settings remain responsive when adding or removing Skills.sh skills, including when Settings is opened from a fresh root draft with no visible session.
- Tool output: valid JSON containing Unicode escape sequences now renders as readable text in both the web UI and terminal UI.
- Web UI: font previews are local and stable, so the picker shows each typeface immediately without late-loading flicker.
grep: directory-component globs now match relative to the requested search path consistently whether ripgrep is installed or the built-in fallback is used.
0.12.2
Fixed
- Web UI: hover tooltips were silently broken on message rows after the mid-word boundary-merge rework — the
PADconstant the tooltip layout depended on had been removed alongside the dead hover-cancel code path. Restored; tooltips on user / agent / reasoning rows, tool cards, and MCP tool rows are back. - Web UI: when a streaming reply was truncated by
max_tokensinside a model-emitted draft answer (i.e. reasoning had already closed and the model was emitting real content for the user when it ran out of budget), the partial answer was being discarded. The last accumulated content is now flushed into its own[agent]block — same way the answer-after-reasoning case was handled in 0.12.1. - Web UI: mid-stream mouse movement no longer cancels a hover tooltip before it appears. The previous
mousemove→ cancel handler kept the tooltip code technically alive but fired on every cursor wiggle and never let the 500ms hover-intent timer complete. Removed; tooltips open on the intended timer and only the standardmouseleave/ scroll / modal-open paths dismiss them. - Web UI: the browser's native tooltip is stripped the moment the cursor enters the trigger element. Otherwise both tooltips render and the native one wins the race on leaving the page, briefly showing the plain
titletext over the styled bubble. - Web UI: custom hover tooltips are suppressed on coarse-pointer / touch devices, where they have no interaction model and were firing on tap-and-release.
Changed
- Built-in
castconfiguration skill: split the 397-lineSKILL.mdinto a short index + sevenreferences/*.mdtopic files (personas, skills, marketplace, mcp, rules, hooks, commands). The skill now follows the agentskills.io progressive-disclosure convention — only the frontmatter + topic map (~1.6 KB) is loaded every timecastis invoked for a config question, and the matching reference file is read on demand. For a focused question (e.g. "how do I add an MCP server?") the model pulls justreferences/mcp.mdinstead of every topic.
0.12.1
Fixed
- Web UI: when a reasoning block ended with a markdown heading (a truncated answer draft that the parser flushed into reasoning because the model ran out of
max_tokensinside<think>without emitting a close tag), the post-heading tail is now rendered as a separate[agent]block instead of sitting inside[reasoning]next to a blank agent area. Applies to live streaming, settled turns, and reload from disk. - Web UI: when the model's
<think>...</think>boundary landed mid-word (observed on MiniMax-M3 emitting</think>inside the Cyrillic word "Сейчас", so reasoning ended "...Сей" and content started "час уточню..."), the trailing word fragment is now glued back onto the agent's content. The user sees the model-intended continuous word — "Сейчас уточню текущую погоду в Астане." — instead of two halves separated across blocks. Same-script check (Latin↔Latin, Cyrillic↔Cyrillic) guards against false merges across alphabets; sentence-ending punctuation inside the fragment stays in reasoning.
0.12.0
Added
Per-persona allowlists for tools, skills, MCP servers, and subagent types. Each persona's frontmatter now supports four knobs that all share the same shape (omitted = no restriction,
[]= explicitly nothing allowed, exact names or*-globs):tools:— built-in tool names (existing).tools: [read, grep, ls, plan_*, web_*]narrows the persona's builtin reach.skills:— skill names invokable via theskilltool.skills: [research, deep-research]hides every other skill from the catalog and the runtime.mcp:— MCP server names (not individual tool names).mcp: [postgres, playwright*]keeps only those servers' tools callable.subagentTypes:— narrowssubagents: truefurther.subagentTypes: [explore, review]is the only set this persona can spawn viatask.
Enforced at runtime (not just in the prompt text) — a disallowed call gets the same "not available" / "not found" / "Unknown subagent" message the model already gets for any hidden tool, so these actually isolate a persona's zone of responsibility.
skills:/mcp:restrictions also forward to anything the persona delegates to viatask, so the restriction can't be routed around by spawning a subagent.Default persona is now
senior(the previouscodingpersona was a strict subset and is removed).coder-with-subagents-force-reviewis also removed — its review gate now lives incoder-with-subagents's own validation pattern.
0.11.1
Fixed
cast web <typo of stop|status>no longer silently starts a new server daemon — any unrecognized subcommand now errors instead of falling through to the default "start" behavior.cast upgrade --<typo of --force>no longer silently attempts to upgrade to a "version" named after the mistyped flag — unknown flags now error before that logic runs./ssh <typo of add|remove>(terminal UI) no longer silently falls back to listing hosts — unrecognized subcommands now error.
0.11.0
Fixed
- Skills.sh: installing via the web UI now actually lands where cast scans (
~/.agents/skills/) — installs used to silently vanish depending on which agent flag was used. - Skills.sh: pasting skills.sh's own "npx skills add ..." command into the install field now works, not just the bare
owner/repo --skill nameargs. - Skills.sh: the installed-skill list now shows the real source repo (read from the skills.sh lockfile) instead of a mis-parsed label that was always empty.
- Skills.sh / SSH: command output no longer leaks raw ANSI escape codes (cursor show/hide sequences) into the UI.
- Settings modal: a failed action no longer leaves every button in the modal permanently disabled.
- Diff/Files panel: a manually resized width now survives a page reload.
- Hooks: a hook that force-stops the turn no longer silently drops
updatedInput/additionalContextcontributed by other hooks in the same run. - Hooks: a
PostToolUse/PostToolUseFailurehook that blocks with no output of its own no longer has its block silently dropped. - Hooks: two identical hook groups from different sources (e.g. a plugin and a project file) no longer share an id, so disabling one no longer disables the other.
- Hooks: a malformed
hooks.jsonnow surfaces a parse-error diagnostic in Settings and/hooksinstead of silently loading as empty.
Changed
- Web UI: Marketplace tab browsing merged into one flat, searchable list across all configured marketplaces instead of per-marketplace tabs.
- Web UI: Skills.sh tab dropped the "list available" (browse-a-repo) field and the network-backed search box.
0.10.1
Fixed
- Hooks:
${CLAUDE_PLUGIN_ROOT}substitution in command strings so plugins shipped with Claude Code's marketplace (e.g. hookify, ralph-loop) load and execute unmodified. - Hooks: case-insensitive tool-name matching in
ifconditions soif: "Bash(git commit:*)"matches payloads where the tool name is lowercase "bash". - Skills:
skilltool description in/skill-instructionsnow explicitly tells the model to call theskilltool (notread) and to load skills FIRST when the request matches a description. - Personas: every persona's Tools section mentions the
skilltool with a generic description (no skill-name examples, to avoid locking in a specific subset).
Changed
- Skills prompt listing now includes
whenToUsealongsidedescription(formatted asdescription — whenToUse) for richer model matching.
0.10.0
Added
- Hooks: full implementation matching the Claude Code protocol — shell/HTTP commands fire on lifecycle events to validate/block a tool call, log activity, or keep the agent working before it stops. Configure via
.cast/hooks.json(project) or~/.cast/hooks.json(global); manage with/hooks,/hooks enable|disable <id>,/hooks help. - Web UI: Settings → Hooks tab, mirroring
/hooks(per-source grouping, enable/disable toggle). - Skills: dedicated
skilltool replaces the model readingSKILL.mdvia the genericreadtool — validates the name, enforcesdisable-model-invocation, and performs argument substitution in one call. - Skills:
$ARGUMENTS,$ARGUMENTS[0]/$0, and${CLAUDE_SKILL_DIR}substitution in skill bodies, plus awhen_to_usefrontmatter field surfaced in the skill listing asdescription — whenToUse. - Web UI: marketplace tabs with a plugin catalog browser (alphabetically sorted tabs and plugins, full descriptions).
Changed
- Web UI: Settings panel restyled — MCP/Skills/SSH intro text switched from a collapsible
<details>to an always-visible summary line; item rows, plugin catalog entries, and theme/font swatches share a consistent card/hover treatment; Plugins tab renamed to Marketplace, settings tabs sorted alphabetically (opens first tab by default). - Web UI: font swatches load their Google Fonts stylesheet so every option previews in its own font instead of falling back to the system font.
Fixed
- Case-insensitive tool name matching in hook
ifconditions. ${CLAUDE_PLUGIN_ROOT}substitution in hook command strings for plugin compatibility.- Hook protocol parity — missing payload fields and output formats now match Claude Code's.
- MCP/Skills/SSH settings tabs now work while the agent is running.
0.9.14
Fixed
- Vision error fallback now catches 400 status codes in addition to 404, so provider image-rejection errors (e.g. oversized base64) trigger image stripping and retry instead of surfacing a raw 400 to the user.
- Per-file image read cap lowered from 25MB to 5MB to match provider limits, preventing images from being embedded only to get rejected at the API.
0.9.13
Internal
- Zero biome lint warnings/errors project-wide.
0.9.12
Changed
todo_writeis now guidance-based — the hard gate that blocked non-todo tool calls after 4 work actions is removed. The model decides when to usetodo_writebased on task complexity, same as opencode'stodowrite.
0.9.11
Fixed
- Web UI: the Inputs tab loading state now uses the same "Loading…" style as every other panel instead of an inconsistent title-sized label.
0.9.10
Fixed
- Web UI: the Inputs sidebar no longer shows a stale "No files attached" flash when re-fetching after a document upload or during agent-response re-renders.
- Web UI: attached documents in a draft session (before the first message was sent) now work — the upload is deferred to
submitMessageaftercommitSessioncreates the real session, instead of blocking with "send a message first". - Web UI: a double
commitSessioncall when drafting a session with pending documents created two separate sessions — files landed in the first, the message in the second, so the Inputs tab was empty while the chat showed the files. A singleidvariable now flows through both blocks. - Web UI: the sidebar's "No sessions match" empty state no longer flashes during initial load before the session list arrives.
Changed
- Web UI: tightened chat vertical spacing — smaller gap, leaner padding, and reduced line-height for denser messages.
0.9.9
Added
- Session search (TUI and web) now runs on a SQLite FTS5 index over full message history instead of a JS fuzzy scorer — same relevance ranking in both interfaces, and a multi-word query now matches across different messages in the same conversation instead of only within one.
web_fetchgets a second backend: "local" fetches the URL directly (no third party sees it) and converts HTML itself, with a Cloudflare-challenge retry, a 5MB response cap, and content-type checks that reject binaries. Switch with/web-fetch-provider jina|local(default staysjina).- Web UI: an "Inputs" tab (right sidebar, ordered Inputs/Files/Changes) for a session's attached documents.
- Web UI: the composer's attach button now accepts documents, not just images — non-image files upload to a session-scoped directory and the model gets told their path via an invisible reminder on send. Executable/binary formats are rejected; archives and ordinary documents are allowed.
cast web's MCP servers now connect in the background after the HTTP server starts listening, instead of blocking startup on every configured server (npx spawns, browser launches, remote handshakes) — the server accepts requests immediately, and connected tools become available in any open session automatically once the connect finishes.
Changed
- Web UI: the "Tools" settings tab split into three — Bash, Web, Quick Mode — each loading only the data it needs instead of five settings commands on every open.
web_fetch: one retry on a transient network failure or 5xx (never on an intentional abort or a 4xx), and truncation now prefers the nearest paragraph break over a mid-sentence cut.
Fixed
web_fetchnow rejects non-http(s) URL schemes (file://,data:, ...) before ever making a network call.cast web status/"already running" could report a server as up before it was actually accepting connections — the daemon state file is now written only once the HTTP server is truly listening, matching its own documented contract.
0.9.8
Fixed
- Web UI: tooltips display as a single horizontal line instead of wrapping into a narrow column due to
max-widthandword-breakconstraints. - Web UI:
assistant_messagehandler now uses event payload (content/thinking/toolCalls) as fallback when streaming blocks are empty, preventing silently dropped messages on SSE interruptions.
0.9.7
Added
- Web UI: custom tooltip system — themed bubbles with fast appearance, viewport-aware positioning, no truncation.
- Web UI: keyboard shortcuts icon (heroicons-compatible outline style).
- Web UI: all settings tabs now have collapsible help sections (Model, Tools, MCP, Skills, SSH, Plugins).
- Web UI: SSH settings now support password authentication (requires
sshpass).
Changed
- Web UI: Settings modal widened from 620px to 820px for better readability.
- Web UI: help sections in Tools and Model tabs now use inline hints after each section title instead of a single collapsible block.
- Web UI: theme swatches redesigned with grid layout — dot left-aligned, label centered.
- Web UI: every theme now carries its own background palette (bg, bgSurface, bgRaised, bgHover, border, borderActive) with improved contrast between UI layers.
- Web UI: 6 themes (Nord, Solarized, Catppuccin, GitHub, Dracula, Gruvbox) use official background colors from their design specifications.
- Web UI: reasoning blocks and turn-meta footer now use
--text-dimfor better readability. - Web UI: chevron icon in collapsible sections replaced from CSS triangle to genuine Heroicon.
Fixed
- Web UI: loading spinners now centered in settings pane, diff panel, and file previews.
- Web UI: Tavily and Brave Search input fields aligned to equal widths.
- Web UI: SSH hosts form reordered — password field before key field.
0.9.6
Internal
- Removed unused
formatActiveRulesPrompt— the per-turn rules path has usedformatRulesForTurn(single combined<rules>block) since it was introduced; the legacy two-part formatter had no callers outside its own tests.
0.9.5
Added
- Two new built-in personas:
researcher(web-search-driven investigation, cites sources) andassistant(general-purpose everyday help — advice, quick lookups, drafts — that reaches for tools only when a task actually needs them). - Web UI: the Files panel's
.mdpreview now renders through a real markdown engine (headings, lists, tables, links) and code files get real per-language syntax highlighting, both vendored for fully offline use. The Settings → Skills "read full content" view got the same treatment, in the same size modal as the file preview. - Web UI: every finished agent reply now shows its own "provider · model · Ns" footer, persisted per-turn — a thread with several exchanges shows a footer under each one, not just whichever was most recent, and it survives a full page reload.
Fixed
- Web UI: a disabled plugin's skill still worked as a native slash command and stayed listed in the composer palette — both now correctly disappear.
- TUI:
/pluginsilently cloned three GitHub-hosted default marketplaces on first use with no way to tell where they came from. They're back as permanent, always-present catalogs (labeled "built-in" in the web UI), but non-removable and no longer a surprise —/plugin marketplace addis the documented way to add your own. - Web UI: Chinese trigger phrases removed from three built-in skill descriptions; the command palette now truncates a long description with an ellipsis instead of letting it overflow the row.
- Web UI: clicking a file's icon or the empty space in its row (not just the filename text) now opens its preview, matching the whole row's hover/click-target styling.
- Web UI: fixed the Files preview's markdown spacing (was inheriting the chat's pre-wrap styling, doubling up as huge line gaps), added Escape-to-close, and removed a raw-then-rendered flash on a slow connection.
- Web UI: the stale "provider · model · Ns" footer and elapsed-time counter from a just-finished session no longer bleed into a brand-new session.
- Web UI: centered "Loading…" states that were pinned top-left instead of filling their container (Settings tabs, Status popover, Files panel's initial load).
- Web UI: a hand-approximated
chevronUpicon path was fixed to match its actual Heroicons source.
0.9.4
Added
- A loaded skill can now be invoked as a native
/<skill-id>slash command in both the terminal and web composer — it shows up in autocomplete with its own description, no/skill:nameprefix needed./reload(and enabling/disabling a skill in the web Settings modal) refreshes the list immediately.
Fixed
- Web UI: Settings could only be opened once a session existed — the gear icon did nothing on a brand-new draft thread. It now always opens; tabs that need a session (Model, MCP, Provider, etc.) show a hint instead of hanging on "Loading…" until one exists.
- Web UI: removed the redundant close button on the Changes/Files panel — the topbar toggle already closes it.
- Web UI, mobile: opening the sidebar while the Changes/Files panel was open (or vice versa) used to leave the one you just opened hidden behind the other, forcing a manual close-then-open. Opening either drawer now closes the other automatically.
0.9.3
Added
- Web UI: the Files panel now shows a live preview when you click a file — text/code renders as-is, images render inline, PDFs open in the browser's own viewer, and CSV/TSV render as a real table (auto-detecting the delimiter — semicolon, tab, or pipe, not just comma).
- Web UI: files and folders in the Files panel can be renamed in place, the same inline-edit as renaming a session in the sidebar.
- Web UI: the Files panel now refreshes on its own — a write/edit that lands while it's open shows up without collapsing and reopening the folder, and the diff/Files panel's open state and active tab now survive a page reload instead of resetting.
Fixed
- Web UI: the Changes/Files panel used to only mount once a session actually existed, so opening it on a brand-new draft thread reserved its column and left the space empty instead of showing anything — it now always renders, with a "No session yet" state instead of a gap.
- Web UI: the Settings tab strip on narrow/mobile screens scrolls but gave no hint that SSH/Theme/Tools were reachable further right — added a fade at both edges.
- Web UI:
.modal-status,.modal-confirm, and.modal-sharewere silently stuck at the same 480px width as every other modal — a stylesheet ordering bug meant their intended (smaller) widths never actually applied.
0.9.2
Added
- Web UI: threads can now be shared as a public, read-only link (
/shared/<token>) — no login required to open it, and it can be revoked at any time. The shared view never includes the persona's system prompt, only the conversation itself. - Web UI: a "Quick session" button next to "New session" starts a fresh thread on a configurable default persona and a clean sandbox directory in one click, instead of going through the persona picker every time. The default persona is set in Settings → Tools.
- The favicon and browser tab title now match the app's actual identity — the tab reads "Cast" instead of "cast web", and the icon is the same pixel-block mark used in the app's own logo instead of a plain letter.
Fixed
- Web UI: the sidebar's session menu (rename/share/delete) no longer opens off-screen for a thread near the bottom of a short or scrolled sidebar — it now opens upward when there isn't room below.
- Web UI: markdown links (
[text](url)and bare URLs) in replies are now rendered as real clickable links instead of plain text. - Web UI: the persona picker under "New session" is no longer clipped with no way to scroll to the rest of the list at high page zoom or with many personas — it now shares one scroll region with the session list instead of a second, cut-off one of its own.
- Web UI: the "Apply" checkmark next to a model/provider picker in Settings no longer stays active when nothing was actually changed, matching how every other picker in Settings already behaved.
- Web UI: a stopwatch tick during every running turn was re-rendering the entire app ten times a second, visible as flicker across the whole page; it's now isolated to just the composer's own elapsed-time display. Settings' model pickers also no longer flash between a cached and a freshly-fetched list on open.
- Web UI: the "not a git repository" message in the Changes panel no longer renders as one lopsided centered line — it's a proper two-line message now.
0.9.1
Added
- Web UI: the final reply now shows a small, muted line with the provider, model, and response time — so a model switch is easy to confirm, and it's clear which model actually answered before deciding whether to switch again.
- The "Coder with subagents + forced review" persona was renamed to "Coder with mandatory review" for a more natural label.
Fixed
- Web UI and TUI: providers that interleave content/reasoning deltas out of order mid-turn (observed on MiniMax-M2) no longer render as broken alternating "agent"/"reasoning" blocks with a word split across the seam — same-kind text now merges back together across the interruption.
- Web UI: switching the model (
/model) now updates the sidebar immediately and becomes the default for every session created afterward — previously a new session always reopened on whichever model was active when the server started, no matter how many times you'd switched since. bash,ssh, and background bash now cap output at true UTF-8 bytes instead of JS string length, matching howreadalready worked — for non-ASCII text (e.g. Cyrillic) the old comparison let through roughly double the configured byte limit. The default cap was also raised from 64KB to 128KB.
0.9.0
Added
- Build mode: a
todo_writetool for tracking multi-step work as a checklist — persisted separately from the message history (survives compaction and process restarts), re-injected into the system prompt every turn, and enforced by the harness: after several tool calls with no list started, every further tool call is refused untiltodo_writeruns. Not available in plan mode, which already has the plan checklist for the same job. - Every persona's tool list now describes
todo_writein its own voice — from qa's "one item per check in your verification pass" to architect's "rarely central here", matching how each persona already describes the rest of its tools. - Web UI: code blocks in the chat now have a copy button (top-right of the block) that swaps to a checkmark on successful copy.
0.8.32
Added
- Web UI: an MCP server row in Settings now has a Reconnect button (next to the pause/resume toggle), so a server that was fixed on disk (new URL/token) doesn't need a full server restart to pick it up.
- Web UI: the sidebar's message-count badges now update live for every thread, including ones you don't currently have open, instead of only refreshing on a full page reload.
- Web UI: the status popover ("i") now shows the active provider and how many tokens were served from cache.
Fixed
- Web UI: numbered/bulleted lists with a blank line between items (common in model output) no longer render every item as "1." — each used to become its own single-item list.
- Web UI: runs of 3+ blank lines in a reply no longer render as an oversized gap between paragraphs.
- Web UI: opening a long thread no longer occasionally leaves the view short of the very bottom — the scroll-to-bottom jump is instant instead of an animated scroll that could lose a race with content still loading in.
- Web UI: streaming replies re-render at most once per animation frame instead of once per token, cutting the worst-case main-thread stall during a long, tool-heavy reply roughly 4x.
- Web UI: the status popover ("i") could fail to open entirely while the agent was running (reading the active provider was wrongly gated behind the same "must be idle" check as switching it).
- A turn cut short by the backend restarting no longer tells the model the user interrupted it — the reminder now says the server restarted instead of misattributing it.
0.8.31
Added
- Web UI: sessions can now actually be deleted — the sidebar row's rename/close icons were replaced with a single ⋮ menu (also opens on right-click) offering Rename and a real, permanent Delete. Previously the × only unloaded a session from memory; it stayed on disk regardless.
- Web UI: a themed confirmation dialog now appears before closing/deleting a thread, matching the rest of the app instead of nothing at all.
- Web UI: new sessions default to a fresh sandbox directory instead of the project root — using the actual project root is now the deliberate choice.
- Web UI: the empty-thread "cast" banner is now a crisp SVG logo (larger than before) that recolors with the active theme, replacing the old ASCII-art text render.
Fixed
- Web UI: closing a thread no longer leaves it as the fallback session when reopening the app with no
?session=in the URL. - Web UI: the Tavily/Brave Search API key fields are now masked, the "Save & use ..." button label no longer renders literally as
&, and a low-contrast hint line was removed. - Web UI: the tool-call result expand/collapse arrow is now a proper icon instead of a tiny text glyph.
- Web UI: the streaming response no longer shows a blinking cursor (the composer's input caret is the only one now).
0.8.30
Added
web_searchsupports Brave Search as a third optional backend alongside DuckDuckGo (default) and Tavily. Unlike Tavily's AI-search aggregation, Brave is an actual general web index — a more direct drop-in replacement for DDG. Configure with/web-search-provider(TUI) or the Tools settings tab (cast web).
0.8.29
Added
web_searchsupports Tavily as an optional backend for anyone hitting DuckDuckGo's scrape rate limit (~4 requests per IP before a CAPTCHA blocks further searches) — Tavily's free tier is a recurring 1000 requests/month instead. Configure with/web-search-provider(TUI) or the Tools settings tab (cast web); DuckDuckGo stays the zero-config default.- Web UI: tool call results are now expandable — click a tool card to see its full output (previously request-only), lazily rendered and capped at 64KB.
- Docs: a new research-grounded page on why persona/role framing changes agent behavior, linked from the personas doc and the landing page.
Fixed
web_search: an empty query or one over Tavily's undocumented 400-character limit no longer fails with an opaque HTTP 400 — empty queries are rejected with a clear message, over-length ones are truncated so the search still runs.web_search: the tool's advertised parameters now match the active backend —region/time(DuckDuckGo-only) are no longer offered to the model when Tavily is active, where they silently did nothing.- GitHub Pages landing page and README: the ASCII banner no longer overflows or visibly snaps to size on narrow mobile viewports — replaced with a static SVG that scales like any other image.
- Docs: corrected two fabricated author citations in the persona-research motivation section.
0.8.28
Fixed
- Web UI: reasoning text no longer mixes into the answer (with stray leading blank lines) when a provider streams
<think>...</think>tags that split across chunk boundaries (MiniMax-M3 and similar) — the tag parser now buffers across chunks instead of scanning each one in isolation. - Web UI: the composer's elapsed-time counter is now legible (accent color, larger) — it's the only signal that a still-running request is alive.
glob:**now recurses into subdirectories instead of matching only the top level.grep: the no-rgfallback now matches--globdirectory components correctly instead of only comparing basenames.edit: no longer corrupts CRLF files into mixed line endings; multipleinsert_after/insert_beforeops sharing one anchor now apply in the model's listed order instead of reversed.write: byte count in the tool result now reflects real UTF-8 byte length instead of JS string length.read:limit: 0is no longer silently treated as "no limit"; output is now also capped by byte size, not just line count.ls: symlinked directories are now classified as directories; missing/non-directory paths return a friendly message instead of a raw error.web_search: a cached query no longer permanently capsmaxResultsfor later, larger requests against the same query.web_fetch: a request whose abort signal was already aborted before the fetch started is now aborted immediately instead of running to completion.ssh: a failure to spawnssh/sshpassno longer crashes the whole process.- Background bash tasks: a spawn failure no longer gets silently overwritten with a bogus "exited" status.
- The
tasktool no longer discards a subagent's usage/cost accounting when the subagent's loop throws. - Ink TUI: the tool-call summary line now counts
insert_beforeedits (previously always showed "+0 -0" for them). - Plan mode:
plan_checkreliably matches real, markdown-formatted plan steps, including a step's own nested sub-bullets, regardless of exact wording. - Plan mode: the "write a plan first" error from
plan_doneno longer references the removedplan_writetool.
Changed
- Plan mode: writing, editing, and reading a plan file now goes through the normal
write/edit/readtools (gated to the plan file's path) instead of dedicatedplan_write/plan_edit/plan_readtools — one less parallel implementation to keep in sync.
0.8.27
Fixed
- The
super-researchskill's description exceeded the 1024-character limit, logging a warning on every startup — it is now trimmed to a concise summary. Added a test that guards every builtin SKILL.md description against the limit so it can't regress.
0.8.26
Added
- Web UI: Settings > Provider — a "Verify credentials" button probes the entered URL + key on demand, and credentials are now always verified before a provider is saved; invalid ones are rejected with the reason (auth / unreachable) shown inline. Backed by a new
POST /api/provider/verifyendpoint, matching the CLI add wizard's probe.
Fixed
- Web UI: switching the active provider no longer leaves stale state — the model list refreshes immediately (no page reload needed), and the subagent/plan pickers no longer show a misleading "(inherits …)" hint for a model that isn't on the new provider. Selected models (main / subagent / plan) are reset on a provider switch since those ids belonged to the old endpoint.
- Web UI: the Model tab pickers now show a consistent "Pick a model…" placeholder for every slot, and the model name is no longer duplicated in the section titles.
Changed
- Web UI: the Provider tab is now a clean list (add / edit / delete). The provider a model slot uses is chosen in the Model tab next to each slot, and subagent / plan now clearly inherit the main model's provider ("same as main") rather than referencing a vague "active (default)" concept.
/provider addmakes the first-added provider the active default so the main model works without a manual switch.
0.8.25
Fixed
- Web UI: chat no longer flickers/remounts when toggling the diff panel, reconnecting, or reloading — an SSE reconnect effect was tearing down and rebuilding every message's DOM node on every diff-panel toggle and on every page load.
- Web UI: the saved theme now applies before first paint instead of flashing the hardcoded default accent while
/api/themesis in flight. - Web UI: personas/commands/themes/config are fetched once per tab instead of being re-fetched (and visibly re-flashed) on every SSE reconnect.
- Web UI: trimmed the cold-load waterfall — dropped an unused Inter font import that was blocking the whole JS bootstrap behind an external round trip, added
preconnecthints, and a reload landing on?session=<id>now fetches that thread in parallel with the session list instead of after it.
Added
- Web UI: Settings > Font — pick from 10 monospace and 10 sans-serif fonts (sans only affects interface text; code/tool output/tables stay monospace) plus a text-scale control (85%–150%), both applying instantly with no server round trip.
Fixed
- Web UI: picking a persona for "+ New session" no longer creates a session on the backend right away — it stages a local draft and only creates the real thread on your first message, like ChatGPT's new chat. Abandoned drafts no longer leave permanent "0 msg" entries in the sidebar.
0.8.22
Fixed
- Sandbox ("new") sessions now create their scratch directory under
~/.cast/sandbox/cast-<id>instead of/tmp/cast-<id>, avoiding tmpfs permission and quota issues. The sidebar button is renamed from "tmp" to "new".
0.8.21
Fixed
- Opening the directory picker after choosing "tmp" in the web UI sidebar no longer shows an ENOENT error — the picker is now only reachable for real, existing paths. The scratch directory for tmp sessions is created server-side at session-creation time and named after the session id (
/tmp/cast-<session id>).
0.8.20
Fixed
- New-session directory picker in the web UI sidebar now shows the current directory and the "tmp" option as a single segmented control, with the active choice highlighted — clearer than the previous cramped label+path+button row.
0.8.19
Added
- "tmp" button next to the directory picker in the web UI sidebar — creates a
/tmp/cast-<id>directory for throwaway sessions.
0.8.18
Fixed
- Web server now defaults to
~as the session working directory instead of the directory wherecast webwas launched. Use the directory picker in the UI to choose a project directory per session. TUI mode still uses the current working directory as before.
0.8.17
Fixed
- Web UI Changes panel now shows all file types including untracked files (previously only tracked-file diffs were visible).
git diff --no-indexexit code 1 no longer silently drops untracked files from the diff response.
Added
- Web UI Changes panel groups files by status: New files, Staged, Modified, Deleted, Renamed — each with a colored dot indicator and section count badge.
0.8.16
Fixed
- Reasoning models that consumed the full
max_tokensbudget on thinking alone would silently fail. Now retries with doubled budget. - Model validation incorrectly rejected reasoning models that return
reasoning_contentinstead of<think>blocks.
Added
- SQLite-backed session persistence — sessions survive restarts.
- Web UI scroll-up pagination for long conversations.
- Turn-accurate message counts in session metadata.
- Full session history preserved across context compaction instead of being pruned.
0.8.15
Fixed
- Large tool results (big file reads, web fetches) could push a session past the context-compaction threshold mid-turn and only get caught reactively once the next model call overflowed. A new guard now compacts right after such a tool result lands, before the next call is made.
Internal
- Unified the three compaction call sites (turn-start check, mid-turn guard, overflow retry) behind one shared helper to keep their message-splicing and event-emitting behavior in sync.
0.8.14
Added
- Shared prompts adapted from an upstream terminal agent: Doing tasks, Executing with care, Tone and style.
- Cast context prompt — all personas now know about cast-specific commands, rules, skills, and plan mode.
Fixed
- Removed duplicate Action safety section from harness-discipline (now covered by Executing with care).
- Renamed cast from "coding agent" to "agent harness" to reflect all persona types.
0.8.13
Added
- New builtin skills from an upstream terminal agent:
arxiv— search, read, cite academic papers from arXivdeep-research— parallel sub-agent research with cited reportsfrontend-design— UI/UX design guidancelearn-everything— interactive learning from documents/PDFssuper-research— autonomous research experiments
- Updated
castskill with nested rules, apply modes table, and full commands reference.
0.8.12
Fixed
- Web UI:
<system-reminder>blocks (compaction, date-rollover, interrupt reminders) now render as styled warning messages instead of raw XML tags after session reload. - Web UI: SSE reconnect — full state sync from server, stale streaming blocks cleared, auto-scroll to latest messages, visibility change detection for mobile tab switching.
- Web UI: message count in sidebar now shows only user and assistant messages (was inflated by hidden tool/system messages).
- Web UI: header "cast" text replaced with themed status dot indicator (green=connected, yellow=reconnecting, red=offline).
- Web UI: ASCII banner no longer overflows on vertical mobile screens — added
white-space: preandmax-width: 100%.
0.8.11
Fixed
- Mobile: ASCII banner no longer overflows on vertical phone screens — font-size reduced to 0.6rem on viewports ≤768px.
0.8.10
Added
- Multi-provider per-model-slot selection: each model slot (main, subagent, plan) can now use a different saved provider. New commands
/subagent-model-providerand/plan-model-provider. Web UI Settings shows cascading provider → model dropdowns for all three slots. - Web UI: skill/plugin full content reader — book icon loads SKILL.md from disk and displays in a centered popover with scroll. Info icon shows short description.
- Web UI: skills grouped by source (Built-in, Global, Project, Plugin) with plugin ID shown for plugin-sourced skills. MCP servers grouped by source (Global, Project).
- Web UI: markdown table rendering in chat messages.
- Web UI: SSH key paste support — paste private key content directly in the SSH form, saved to
~/.cast/keys/with 600 permissions. - Web UI: provider edit support — edit button pre-fills URL/API key for modification.
- Web UI:
user_messageSSE broadcast — user messages from one tab appear in all other tabs viewing the same session. - Web UI: new block-char ASCII banner (
░▒▓█) for both TUI and web.
Fixed
/reloadskips MCP reconnect when config unchanged — 2.7s → 27ms (100x faster)./queue-reset//qrnow clears pending queue badges in the web UI.- ESC key closes info popovers before settings modal (proper layered close via
stopPropagation). - Markdown tables now render as
<table>elements in web UI chat. - Provider tab no longer crashes with
i.push is not a function(htm&&→ ternary fix). - Models load instantly in Settings via
/api/models/cached(no network call on open). - All icon buttons use unified cyan hover accent; focus outlines removed.
- Pending steer (cyan) and queue (amber) items color-differentiated above the composer.
Changed
- Web UI Settings modal: fixed height with scroll (no size jumping between tabs).
- Web UI: all text buttons replaced with verified Heroicons v2.1.5 icons with title tooltips.
- Web UI:
GET /api/models/cachedreturns cached models instantly;GET /api/models?provider=<name>fetches from a specific provider. - Web UI: SSH form fields stacked vertically with key paste textarea.
- Web UI: plugins show
pluginname +marketplaceas meta; skills showname+source/pluginIdas meta.
0.8.9
Added
- Web UI: real-time sidebar updates — status, title, and message count changes are now broadcast to all connected browser tabs via SSE
session_updateevents, eliminating the need for manual refresh. - Session summary index expanded with persona, model, title, pinned, and createdAt fields — cold session sidebar rows no longer require parsing full session JSON.
- JSONL session persistence — messages are now appended incrementally to
.jsonlfiles instead of rewriting the entire session JSON on every mutation. Legacy.jsonsessions are auto-migrated to JSONL on startup.
Fixed
- DECXCPR cursor-position queries (
\x1b[6n]) no longer leak as visible escape sequences when stdin is not in raw mode (e.g. duringsuspendTerminal, tmux focus-out). <Static>items now use stable WeakMap-based keys instead of index-based keys — messages no longer disappear after compaction, steering injection, or session switch.- TUI: StatusBar extracted into its own component with local 200ms tick — elapsed-time updates no longer re-render the Composer area.
- TUI: ChatLog wraps useWindowSize in its own component — resize events no longer cascade re-renders through the Composer.
Changed
- Web UI:
session_endSSE event carries usage and message count — the frontend only refetches the full session when message counts diverge (reconnect recovery), skipping the fullGET /api/sessions/:idon normal uninterrupted runs. - TUI:
/model,/plan-model,/subagent-model,/provideruserefreshMeta()instead ofrefresh()— no unnecessary message rebuild when only config metadata changes. toDisplayMessagesuses O(M) Map lookups instead of O(N×M)messages.find()for tool result matching.- TUI:
maxFpsreverted from 60 to 30 — double write frequency increased desync/flicker odds on slow terminals (SSH, tmux, mobile emulators).
0.8.8
Fixed
- Web UI: page refresh during a running agent turn no longer loses the final assistant message — the
endevent now merges server-persisted messages into the client state. - Web UI: pre-run save ensures the user's message survives a mid-run process kill (SIGTERM timeout, OOM, crash).
- Web UI: elapsed timer pauses when the SSE connection drops instead of counting up with a stale connection.
- Web UI:
statusevent on SSE reconnect now refetches messages if the run completed while the client was disconnected. - Web UI:
fetch()andEventSourceURLs now usewindow.location.originexplicitly to avoid cross-origin issues behind reverse proxies.
0.8.7
Fixed
- Web UI static files (HTML, CSS, JS) now ship in release installs —
cast webwas returning 404 becausedist/public/wasn't included in the archive. cast web --foregroundcorrectly uses the specified--portinstead of always defaulting to 1337.
0.8.6
Fixed
cast webdaemon spawn now works in release installs (no longer requirestsxor TypeScript sources).cast web --foregroundruns inline instead of spawning a child process.
0.8.5
Added
cast webnow detects already-running instances and refuses to start a duplicate (instead of silently spawning a second server on the same port).cast web --public/--host 0.0.0.0— bind to all interfaces for network access (prints a security warning).cast web stopgracefully shuts down open sessions (SIGTERM), escalating to SIGKILL after 3s. Detects and cleans up stale state when the process is already gone (crash, OOM,kill -9).cast web statusauto-heals stale PID files — reports honestly instead of claiming a dead process is running.- Built-in
skill-creatorskill (user-invoked): reference for writing predictable skills, based on Matt Pocock's methodology (invocation modes, information hierarchy, leading words, pruning, failure modes).
0.8.4
Added
- New shared prompt section (
verification-discipline.md) instructs the agent to verify changes against the real running interface rather than trusting tests alone — appended to every persona and subagent prompt.
Fixed
- Search tool (
grep,glob) no longer blocks the Node event loop whilefd/rgrun — switched from sync to async subprocess execution, which matters under concurrent tool calls.
Internal
- Eval harness restructured into
benches/(basic, hashline, mutation) +lib/(runner, fixtures, results, trace-view). - Eval runner now supports model comparison (
--compare,--compare-x3) and trace replay (--trace). - Added mutation bench for measuring tool robustness against prompt perturbations.
- Added eval methodology doc (
docs/eval-methodology.md).
0.8.3
Changed
- All persona prompts now consistently mention conditional tools (ssh, background bash) via the "go by your actual tool list" note —
coding.mdandsre.mdwere missing it.
0.8.2
Changed
- Persona prompt tool lists no longer hardcode
ssh— they now point the model at its actual tool list, so tools that are only conditionally available (ssh, background bash) aren't advertised when absent.
0.8.1
Added
- Background bash tasks: the
bashtool gains arun_in_backgroundparameter (web and TUI only). Setting it totruespawns the command without blocking and returns a task id immediately. Completion arrives automatically as a<system-reminder>— no polling needed. Two companion tools (bash_output,bash_kill) let the agent check progress or terminate a task early. Background tasks survive across turns and are session-scoped; they're reaped on session close.
0.8.0
Added
- Web UI (
cast web): browser-based control room for managing background agents. Creates sessions with different personas, streams responses token-by-token, shows tool calls as terminal-style cards, and includes a resizable git diff viewer. Settings modal covers model/reasoning, theme, web tools, bash confirmation mode, and MCP/skills/plugins/provider/SSH management. Non-blocking slash commands (/help,/current,/usage) work while the agent runs. Keyboard shortcuts reference (Ctrl+//⌘/) for sidebar, diff, new session, and clear-context. Auth with auto-generated password. Same sessions persisted to~/.cast/sessions/as the TUI.cast web— start in background (daemon)cast web stop/cast web status— manage the servercast web --foreground— run inline for dev/debug- Default port 1337, configurable via
--portorCAST_WEB_PORT
0.7.12
Fixed
- Ink incremental rendering enabled (
incrementalRendering: true) — only repaints lines that actually changed, reducing terminal traffic and eliminating flicker on frequent redraws. Frame rate cap raised from 30 to 60 FPS for a more responsive composer.
0.7.11
Fixed
- Removed focus-reporting mechanism (
\x1b[?1004h) that triggered a terminal resync on alt-tab: some terminals send focus-in reports unprompted, causing spurious screen clears that wiped the banner and broke the viewport. Focus in/out sequences are still silently dropped by the input parser so they never surface as stray characters.
0.7.10
Fixed
- Autoscroll breaks after Alt+Tab: switching away from the terminal and back no longer leaves the viewport stuck — resync respects the current scroll position instead of force-resetting it.
Changed
- Shift+Tab keybinding removed from Composer — simplified input handling, removed dead code from input-parser and keybindings.
0.7.8
Fixed
- Terminal rendering corruption on Termius (mobile) and similar terminals: Ink's per-line erase sequence (
\x1b[2K\x1b[1Arepeated) is now coalesced into a single combined cursor-up + erase-to-end-of-screen before writing, preventing orphaned top-border fragments from stacking on every keystroke.
0.7.7
Added
/continueslash command — resumes the most recent session without leaving the current one. In-session equivalent ofcast -c.
Fixed
- Streaming output no longer overflows the viewport when the composer grows taller than the static budget estimate: ChatLog now shrinks its live-region budget reactively based on the actual last-frame overflow, so one bad frame self-corrects instead of repeating every frame.
- Composer ghost rows (streaks, duplicated borders) after deleting multi-line input: the frame now uses a sticky max height and pads back on shrink instead of letting Ink leave stale rows on screen.
- Synchronized-output flash on terminal resync (clear + replay): both writes are now wrapped in CSI ?2026h/l so the terminal buffers and swaps atomically.
- Resync no longer fires immediately after an aborted turn (Esc): the disruptive full clear + scrollback wipe lands on the next turn that actually completes instead.
- Reasoning and content block labels (
[reasoning],[agent]) no longer repeat on every split-off line of the same streaming run. - Edit tool results no longer shown inline in ChatLog (same treatment as
read). - Composer re-renders on every stdin chunk even when nothing changed (DECXCPR responses, focus reports, partial escapes): now skipped unless the buffer value or cursor position actually moved.
- DECXCPR poll rate adapts: 200ms during streaming or when a resync is pending, 1s at idle to reduce unnecessary terminal traffic.
- Spinner render cycles reduced ~35% (120ms interval vs 80ms) with no visible quality loss.
Internal
splitCompleteLinesdrains completed lines from the trailing streaming block incrementally, giving the final answer the same steady commit cadence reasoning already gets.useTerminalResyncscroll flags (scrollUp,scrollUpStale) reset after a resync clear, so the next Ink frame isn't swallowed by the scroll guard.- Vitest
NODE_OPTIONS=--no-deprecationsuppresses the punycode warning fromopenai -> node-fetch -> whatwg-url.
0.7.5
Added
- Personas travel with the thread: each session remembers the persona that drove it, and resuming (
-c,--resume,/sessions) restores it — same rule as plan/build mode. The global setting remains the default for new sessions; a deleted persona falls back to the current one with a notice. - Switching to a different persona (
/persona) in a non-empty thread now offers to start a new session, so the previous persona's context doesn't bleed into the new role; "Continue here" / Esc keeps the current thread. - Four new built-in personas rounding out the IT-company role set:
architect(trade-off analysis, ADRs, module boundaries),analyst(requirements from vague asks, contradictions, API contracts),sre(incident response, blameless postmortems, SLOs), andproduct(hypotheses, success metrics, prioritization — distinct from the ticket-writing Project Manager). - Built-in persona
coder-with-subagents-force-review(Coder · forced review): same delegation ascoder-with-subagents, plus a mandatory review gate — every code change goes through an independentreviewsub-agent (fresh context, diff-based input, execution-confirmed findings, exactly one round) before being reported done. No "too trivial to review" exception for code.
0.7.4
Fixed
- Provider requests fail on Node 24 with "Cannot connect … (invalid content-length header)": the OpenAI SDK sets an explicit
content-lengthheader, which is a forbidden fetch request header — Node 24's undici rejects the request outright (Node 26 silently ignores it). cast now strips it and lets the runtime compute the value; model selection/chat work on Node 24 again.
0.7.3
Fixed
- Windows:
cast upgradeno longer crashes on exit withAssertion failed: !(handle->flags & UV_HANDLE_CLOSING)— the hardprocess.exit()right after the release-check fetch raced libuv's handle teardown; the command now returns and lets the process exit naturally.
0.7.2
Fixed
- "Cannot connect to
" errors now include the underlying network detail ( ECONNREFUSED/ENOTFOUND/ certificate errors, including ones buried in undici AggregateErrors) — DNS, dead-endpoint, and TLS-interception failures need different fixes and were indistinguishable. - Windows: the Git Bash registry probe no longer leaks
reg.exe's localized stderr into the TUI as mojibake when the GitForWindows key is absent.
0.7.1
Added
- Fuzzy search in the session picker (
--resume,/sessions): type to filter by project path, session id, or any user/assistant message text in the thread; substring matches rank above subsequence (typo-tolerant) matches.Escis the only cancel key while searching —qgoes into the query. writereplies with a line diff vs the previous content (plus trailing-newline notes) instead of a byte count; new-file and identical-content cases are reported explicitly.writeandeditwarn when the resulting file contains consecutive identical lines — the classic symptom of a duplicated-line botch.editauto-recovers a stale anchor that matches a run of contiguous byte-identical duplicate lines (they're interchangeable), instead of dead-ending with "multiple lines match".- Windows: the
bashtool locates a native Git Bash (CAST_BASH env override → GitForWindows registry key → known install paths incl. no-admin and scoop → derivation fromgiton PATH) instead of picking up the WSL shim from PATH, which loses output. Falling back to PATH bash warns at startup and in the first tool result. - Session summary index (
~/.cast/sessions/index.json): the picker lists hundreds of sessions from an mtime-validated cache (~5ms warm) instead of parsing every session file; the full session is parsed only for the one you pick. Self-healing — safe to delete. cast -cfinds the most recent session by file mtime and parses only that file (was: parse everything).
Fixed
- Prompt-cache markers (
cache_control) no longer leak into saved sessions:applyCacheControlworks on request-only copies, and loading normalizes sessions damaged by older builds — fixes opaque 400s ("Can only get item pairs from a mapping") when resuming after a provider switch. - Resuming a session created on a different provider falls back to the currently configured model (with a notice) instead of sending requests to a model the new provider doesn't serve.
- Tool-call arguments that are valid JSON but not an object (e.g. a bare array) are wrapped before sending, so providers whose chat template iterates arguments as a mapping don't reject the whole history.
- Stdio MCP servers inherit cast's full environment (config
envwins) — shell-exported API keys now reach servers; the SDK's whitelist default silently stripped them. - Remote MCP servers that only speak the legacy HTTP+SSE transport now connect: Streamable HTTP is tried first, then one SSE retry on rejection. SSE JSON-RPC POSTs run on a dedicated connection pool — the long-lived
/ssestream otherwise serializes them behind itself in Node's fetch and the handshake hangs forever. - SKILL.md / persona / rules frontmatter survives a UTF-8 BOM (Windows Notepad,
Out-File); previously the whole frontmatter was silently discarded. - Plugin marketplace commands report "git is not installed or not in PATH" instead of a raw
spawn git ENOENT; staging directory names derived from Windows local paths no longer contain\or:; marketplace install retriesrm/rename against transient Windows EPERM/EBUSY locks. getMostRecentSessionskips a corrupt (half-written) newest session file and falls back to the next one.bashtool reports a clean error when the bash executable itself can't be spawned (e.g. a wrongCAST_BASH), instead of hanging.
Changed
- Docs: provider credentials are configured only via
~/.cast/settings.json//provider— thePROVIDER_BASE_URL/PROVIDER_API_KEYenvironment variables were documented but never read; the docs no longer claim otherwise. - Minimum Node.js version raised from 18 to 22 (required by undici 8.x used for MCP SSE transport).
0.7.0
Added
- After compaction (auto or
/compact), cast injects a separate trailing<system-reminder>user message with edited files and a TODO list of open plan steps. Steps come from- [ ]checkboxes when present, otherwise from###headings under## Steps(common in real plans). Omitted when there is nothing actionable; summary text stays reminder-free. - Turn-end open-work gate in build mode with an active plan: if the model stops without tool calls while plan steps remain open, cast injects a
<system-reminder>and continues sampling (up to 2 times per user prompt, then falls through with an exhausted notice). - After a mid-stream
/abort(Esc) with no tool-result abort signal, cast appends a<system-reminder>([Request interrupted by user]) so the next turn’s model sees that the prior turn was cut off. - Overnight sessions get a one-shot
<system-reminder>when the local calendar date advances past the last announced day (persisted per session). - Built-in
exploreandreviewsubagents fortask(read-oriented tool allowlists).coder-with-subagentssteers mapping toexploreand validation toreview;workerremains the default catch-all for everything else. - Marketplace plugins (Grok/Claude-shaped):
/plugin marketplace add,/plugin install name@marketplace— installs contribute skills from~/.cast/plugins/. /skillsand bare/pluginopen multi-select toggles (same UX as/mcp); disabled skill names persist indisabledSkills.- Default marketplaces auto-seeded once: Codex (
openai/plugins), Claude (anthropics/claude-plugins-official), Grok (xai-org/plugin-marketplace). /pluginslash palette lists install / marketplace / toggle subcommands; builtincastskill documents plugins + toggles.- Bare
/plugin uninstallopens a picker + confirm (typedname@marketplacestill works). /skills uninstalland/mcp uninstall— interactive picker + confirm (or typed name); removes global/project skills and mcp.json entries, clears matching disable flags, hot-reloads.- Uniform
/skills//mcp//pluginsurface:list,enable/disable,help; toggle cancel shows[Cancelled]; no-op toggle skips reload; typed uninstall confirms;/plugin marketplace removecleans settings + reloads skills. - Skill discovery loads skills.sh universal paths:
.agents/skills/(project, trust-gated) and~/.config/agents/skills//~/.agents/skills/(global), sonpx skills add … -a universalworks without copying into.cast/skills/. /skills,/mcp, and/pluginpickers/lists sort entries alphabetically by name (skills were previously discovery-order, so plugin skills clustered at the bottom)./skillslabels plugin skills with their pack id (plugin · name@marketplace). Skills from a disabled pack stay visible but locked (muted) until/pluginre-enables the pack./skills uninstalllists plugin skills as muted/locked (remove the pack via/plugin uninstall); Enter on those rows is ignored.
Fixed
readtool rows show the correct 1-indexed line range (was off-by-one whenoffsetwas set).- Live-region
taskrows stay one-line (truncate) while streaming so parallel tasks remain visible; full wrapped assignment still shows once promoted to history. - Committed
tasktool rows show the full subagent report (wrapped), not a 500-char truncated line. - Session rebuild/resume restores tool
[error]via persistedcastIsErroron tool results (was always[ok]). - Trackpad scroll during an active agent turn no longer fights Ink redraws: while the live region fits the screen, cursor-position polling stays on (and short CUU frames cannot clear the scroll-up guard); tall streaming frames skip that poll so a false scroll latch cannot swallow redraws and scramble scrollback.
- Sync
tasksubagents honor parent--no-skills/--skill(they previously always loaded global/builtin/plugin skills and ignored CLI skill paths).
Changed
- Docs spell out hot-reload vs
/reload:/skills//mcp//plugintoggle and install/uninstall apply in-session;/reloadis only for on-disk file drops/edits (same chat, no restart). /skills//mcp//pluginpickers put the full description on the focused second line (wrap), not truncated into the label.taskUI shows the delegated assignment text (not raw JSONkey=valueargs). Non-default subagent names are prefixed (explore · …).- Subagent final-answer extraction ignores empty placeholder turns (
(no response)); the worker prompt requires a standalone closing report. - Sync
tasksubagents now receive the same environment grounding as the parent: Current System State (cwd/date/platform/model), always-apply + lazy rules, skills catalog, MCP server list, and SSH hosts. --no-skillshelp/docs clarify that plugin skill discovery is skipped too (behavior unchanged).coder-with-subagents(and thetasktool description) steers harder on user cues like “parallel” / “independently”: split into same-turn multi-taskcalls instead of solo exploration.- Shared prompt append adds Agent discipline (action safety, parallel tool calls, preamble-with-tools, prompt secrecy) for all personas and subagents.
0.6.12
Changed
- Renamed the file-search builtin from
findtoglob(same glob-pattern behavior). Legacyfindcalls andtools: [find, …]allowlists still work. - Shared file-tool guidance steers named-file tasks to
readfirst; shortglobresults remind the model toreada hit instead of another search/ls. editinsert_afteraccepts anchorEOFto append at the end of a file (alongside existing0:for the top).
Fixed
editrecovers unique hash-only anchors when the model omits the line number (local:chunkinstead of22:local:chunk), and accepts ASCII->gutters the same way as→.- Shared file-tool guidance (all personas and subagents) now spells out the read→edit workflow: known path skips
glob, oneeditper file, copy the full three-part anchor, retry from tool-returned anchors instead of re-searching. - When
read/editmiss a path, cast runs a basenameglobunder the hood and lists real matches so the model can retry the correct path without starting its own search loop.
0.6.11
Added
- Persona and subagent frontmatter support
tools(builtin allowlist; exact names or*-globs likeplan_*/web_*) andagentsMd(defaulttrue). Omittoolsfor all builtins; MCP tools are never filtered by the allowlist. Session gates (plan/build mode, web toggle) still apply on top.
0.6.10
Fixed
editreturns edited regions with fresh anchors on success, so a follow-up edit on the same file no longer needs a re-read after lines shift under prior anchors.readoutput was switched from the two-part<LINE>:<HASH>→anchor format to the three-part<LINE>:<LOCAL>:<CHUNK>→format (introduced during the 0.6.9 cycle). The three-part form gives finer-grained movement detection when lines shift around, and the stale-anchor error path now returns a fresh anchor instead of failing blind.
0.6.9
Added
editnow acceptsinsert_beforeto add new lines above a target anchor (in addition to the existinginsert_after). Useful when the natural reference is a heading you want content to sit above rather than below.- Successful
editoperations now return the edited regions with fresh anchors, so a follow-up edit doesn't need a re-readeven when prior anchors shifted.
0.6.8
Changed
- Hashline anchor format switched from
<LINE>:<HASH>to<LINE>:<LOCAL>:<CHUNK>forread/edit/grepoutput. The three-part form gives finer-grained movement detection when lines shift around, and the stale-anchor error path now returns a fresh anchor instead of failing blind. Anchors emitted under 0.6.7 are no longer valid; re-read the file to get anchors in the new format.
Fixed
parseAnchornow ignores any content past the→separator, so pasting areadgutter line (with its arrow and trailing content) intoeditproduces the correct anchor instead of a malformed one.
0.6.7
Changed
readoutput now carries hashline anchors in the form<LINE>:<HASH>→contentso a line reference made in one assistant turn still points at the same line after a re-read;editacceptsreplace/insert_after/writeoperations keyed by those anchors and validates the whole batch atomically against the current file (stale anchors return fresh anchors instead of failing blind).
Internal
- Per-line hash computation for
read/edit/grepis now backed by an in-memory LRU (default 20 entries, ~4 MB worst case). Entries are re-validated against filemtimeon every access, so cache hits silently invalidate after external edits. Lruexposes asizegetter so the test-onlyhashlineCacheSizeno longer reaches into a private field.
Internal
read/editnow emit and accept hashline anchors (<LINE>:<HASH>→…) so line references in the conversation survive re-reads; edits are validated atomically against the file as it stands at edit time- In-memory LRU (default 20 entries, ~4 MB worst case) caches per-line hashes for
read/edit/grep; entries are re-validated against filemtime, so cache hits silently invalidate on external edits - Added a public
sizegetter on the LRU sohashlineCacheSizeno longer reaches into a private field
Changed
/currentmodel line shows the configured model and the live plan model together when plan mode swaps in a separate one, so the discrepancy from the status bar becomes visible instead of silently different.
Internal
/currentrendering moved intoformatValueon each registered status bar segment — adding a new segment now needs one place instead of two.applyProviderSelectionextracted from/provider activate— the post-save flow (selectModel→selectReasoningLevel→refresh) lives in one helper./provider addkeeps its own notice wording and stays inline./currentand/usagereuseabbreviateTokensandformatContextPctfromApp.tsx— localfmtK(no M-branch) and a duplicated context-percent formatter are gone.- Hermes XML strip is a single function (
stripHermesToolCalls) shared bycore/llm.tsand the streaming path — the previously duplicated private copy was deleted. ensureConnectionAlivenow writes the full providers array (not just legacyproviderUrl/apiKey); the regression test was tightened to assert exactly what gets persisted, including that existing providers survive a reconnect prompt.- New
test/statusbar.test.tscoversdefaultStatusBarConfig, theSEGMENT_MAX_WIDTHoverflow map, and the empty-data paths of the registered renderers; newformatValuetests cover the plan-mode model divergence.
0.6.5
Added
- Multi-provider support —
/providernow opens a picker to switch between saved providers;/provider addadds a new provider (name → URL → key wizard);/provider deleteremoves one. Providers persist insettings.jsonand the active one is remembered across sessions.
0.6.4
Fixed
- Hermes tool-call recovery — XML
<function=…>blocks in assistant prose (e.g. the model describing the feature itself) are no longer mis-parsed as live tool calls, preventing400 Param Incorrectloops on the next request. Recovery and dedup-strip now gate on actual tool names from the current session.
0.6.3
Fixed
- Streaming dedup — Hermes models that emit both XML
<tool_call>blocks and native function-calling in the same response no longer produce duplicate tool invocations
0.6.2
Added
/statusbarcommand — toggle, reorder, and reassign status bar segments between left/right sides via an interactive picker. Config persists across sessions. Useful on narrow/mobile terminals where the full bar overflows. Default: persona, mode, model (left) and elapsed (right); toggle others via/statusbar./currentcommand — show all status bar data in a list, including disabled segments
0.6.1
Fixed
- Terminal resync — resize and focus-regain now use a light clear that preserves scroll position; theme changes and streaming desyncs still do a full scrollback wipe
- lineChurn — O(m·n) fallback for large edits uses Set-based comparison instead of raw block count; identical large texts no longer report false positive changes
- Input parser — DECXCPR cursor-position responses (
\x1b[row;colR) explicitly dropped to prevent accidental keybinding matches
Internal
displayWidthextracted tosrc/ui/display-width.tswith per-session cache- Test directories isolated to prevent parallel test collisions
0.6.0
Added
- SSH tool — run commands on remote hosts via SSH; hosts configured in
~/.cast/ssh.json(global) or.cast/ssh.json(project) /queue-resetalias — shortcut for clearing the command queue
0.5.8
Added
- MCP server toggle —
/mcpnow opens an interactive multi-select picker to enable/disable individual servers mid-session. Disabled servers are hidden from the model and persisted in settings. <available_mcp>block in the system prompt — the model sees only enabled MCP servers and their tools, and will not attempt to call disabled ones.- Hermes XML tool-call parsing and recovery
- Terminal desync tracking with automatic resync on focus return
0.5.7
Added
- Enhanced search functionality with permission handling and output notes
- Improved tool name sanitization to prevent doom loops
0.5.6
Fixed
- Terminal tools (
plan_done,plan_enter) now force-end the turn — prevents the model from keeping runs alive by rewording summaries to dodge the doom-loop detector plan_doneno longer echoes full plan content into model context, which invited endless "refinement" loops
0.5.5
Added
- Changelog page with version history from 0.1.0 to 0.5.4
- Sequential prev/next navigation on all documentation pages (reading loop: Getting Started → ... → Changelog → Getting Started)
/usagecommand documented in README and interactive commands reference- Plan mode refine option uses the regular composer (multi-line and image paste supported)
Fixed
- Improved provider error classification — OpenAI SDK
APIConnectionErrorcause chain is now fully traversed for accurate error reporting
0.5.4
Fix: ensure non-negative token counts in usage tracking and streaming.
0.5.3
Fix: /usage now correctly shows sub-agent token breakdown.
0.5.2
Added
/usagecommand — show cumulative session token/cost usage/exitcommand — alias for/quit
0.5.1
Changed
- Repositioned cast as a "role-based agent harness" — 13 built-in personas, same tools, different judgment
0.5.0
Added
- Plan mode —
/planenters a read-only exploration phase; the agent studies the codebase and writes a structured execution plan with a checklist - Plan tools:
plan_write,plan_edit,plan_read,plan_done,plan_discard,plan_enter,plan_check - Per-phase model support —
/plan-modelsets a separate model for planning vs building - Plan files persist as markdown in
~/.cast/plans/; survive compaction and session restarts - Approval dialog: implement now, clear context + implement, approve for later, or refine
- E2E smoke test for plan mode
Changed
- Comprehensive documentation overhaul — all features now documented in
docs/
0.4.7
Added
- Improved picker viewport handling with scrolling and index clamping
- 8 new dangerous bash patterns (fork bombs,
shutdown,npm publish,killall, etc.)
0.4.6
Changed
- Enhanced dangerous command detection in permissions
0.4.5
Changed
- Removed interactive command checks from permissions (simplified)
0.4.4
Added
- Web tools —
web_search(DuckDuckGo) andweb_fetch(Jina Reader) for internet access - Web tools are off by default; toggle with
/web(persists to settings)
0.4.3
Added
- Doom loop detection — blocks a tool after 3 identical consecutive calls with the same arguments
Fixed
- Streaming viewport clamping and scroll position issues
0.4.2
Added
/copycommand — copy last assistant response to clipboard
Fixed
- Scroll position not resetting on resync while user is scrolled up
0.4.1
Fixed
- Atomic writes for session and settings files (prevents corruption on crash)
- Session listing and MCP connect timeout hardening
- Clear error on missing required prompt files
- Persona sorting uses label instead of name
Changed
- Split
tools.tsinto per-tool modules (bash.ts,files.ts,search.ts,web.ts,task.ts) - Centralized prompts directory resolution
0.4.0
Added
- Multi-source personas — project-local, global, and builtin with priority ordering
- Sub-agent support via the
tasktool — delegate work to isolated sub-agents coder-with-subagentspersona
0.3.17
Added
- Brace expansion in glob patterns
- Enhanced gitignore handling
Changed
- Agent loop and UI performance tracking improvements
0.3.16
Added
- Non-interactive mode —
cast runsends a single prompt, streams to stdout, exits --format jsonfor structured JSONL output
0.3.15
Added
/repocommand — show cwd, git branch, dirty state, remote, and HEAD- Multiple color themes (16 total)
0.3.13
Added
- Theme support —
/themepicker, persisted to settings
0.3.12
Fixed
- Made node-pty optional with pipe fallback for release bundles
0.3.11
Added
- PTY for bash commands — captures interactive prompts (e.g.
npm init)
0.3.10
Fixed
- Live bash command reveal only when waiting for input
0.3.9
Added
- Re-pick model when provider token changes at startup
- Custom model id entry in picker
- Surface actionable turn errors (revoked key, quota exceeded, no access)
- Recover from dead provider connection at startup
- Esc stops the running turn; Ctrl+C exits with confirmation
Fixed
- Provider key persistence on re-entry
- Distinguish aborted, disconnected, and completed turns at stream end
0.3.8
Changed
- Streaming and rendering logic overhaul for chat messages
0.3.7
Added
- StdinManager for handling interactive input in child processes
Changed
- Streamlined session handling
0.3.6
Fixed
- Inline model context windows map (removed external JSON file dependency)
0.3.5
Changed
- Documentation updates
0.3.4
Fixed
- Message sanitization and tool call handling
0.3.3
Fixed
/steerbehavior when idle (now submits as normal prompt)
0.3.2
Added
- Paste chip functionality in Composer
- Command aliases:
/sfor/steer,/qfor/queue - Nested context file resolution (AGENTS.md in subdirectories)
0.3.0
Added
- Rules system — Cursor-compatible
.cast/rules/*.mdwith always/auto/lazy/manual modes @rule-namementions in messages- Chat log display improvements (clampTailToRows)
0.2.3
Added
- System state block in system prompt (model, reasoning, cwd, git branch)
0.2.1
Added
/keyscommand — list all keybindings
0.2.0
Added
- Multi-source personas (project > global > builtin)
- Built-in skills
- Cast meta-skill for self-configuration
0.1.4
Fixed
- Token count abbreviation in status line (8.7k, 1.2M)
0.1.3
Added
/modelhighlights current selection in picker- Honest reasoning display
0.1.2
Fixed
reasoning_contentfield support- Resize reflow
- Tool call summaries
/steerand/queuevalidation
0.1.1
Fixed
- ThinkBlockParser off-by-one
- Added QA personas and vendors tests
0.1.0
Initial release. Ink TUI, 13 built-in personas, OpenAI-compatible provider, session persistence, context compaction, MCP servers, skills, parallel tool execution, sub-agents.