Releases: SafeRL-Lab/cheetahclaws
Release list
v3.5.85
- July 10, 2026 (v3.5.85): REPL quality-of-life — completion works on every install,
/modelgets a Tab picker, and sessions autosave every turn. Three related changes. (1)prompt_toolkitis now a core dependency. The typing-time completion menu (slash commands, subcommands, the new/modelpicker) is driven byprompt_toolkit, but it was an optional extra ([autosuggest]), so only environments that happened to already have it — e.g. a fat Anaconda base — got the rich experience; a cleanpip install cheetahclawsor an isolateduv tool install cheetahclawsfell back to bare readline (Tab-only, no live dropdown). Since the interactive REPL is the product,prompt_toolkit>=3.0.43moved from[project.optional-dependencies].autosuggestinto[project].dependencies(and into the core block ofrequirements.txt), so every install method now gets live completion out of the box. The readline fallback path inui/input.pyis untouched — it still covers any environment whereprompt_toolkitgenuinely can't be installed. The[autosuggest]extra is kept as a harmless no-op alias for backward compatibility. (2)/modeldynamic completion (PR #166). Typing/modeland pressing Tab now offers aprovider/modelpicker — one default model per configured provider plus a two-levellitellm/<backend>/<model>tree you can drill into — instead of forcing you to remember and hand-type long model strings. Completions are context-aware (/model openai/gnarrows to OpenAI models;litellm/openrouter/expands that backend). Wired into both theprompt_toolkitand readline completers via a new dynamic-completions registry; thelitellmprovider also gained a small curated starting model list to seed the picker (any valid LiteLLM string still works regardless of the list). (3) Per-turn crash-safe session autosave. Previously the live transcript was written tosession_latest.jsononly on a clean exit /Ctrl+C/ budget-pause, so a power-loss or hard kill mid-conversation lost everything since the session started (file edits and explicit/rememberwrites were already immediate, so only the transcript was at risk). A newautosave_session()(incommands/session.py) is now called at the end of every turn inrun_query: it rewrites onlysession_latest.jsonvia a temp file +flush()+os.fsync()+ atomicos.replace()(durable against a power cut, and a crash can never leave a half-written file), stays silent (no console spam), reuses one stablesession_idso each turn overwrites the same file, and is best-effort (never raises into the REPL). It deliberately does not write adaily/copy, append tohistory.json, or touch SQLite — those remain exit-time finalization steps insave_latest(), which still prints the loudSession saved → …paths on quit. Net effect:/resumenow recovers a conversation after a crash, not just after a clean exit. See docs/PR/resume_Feature.md · docs/guides/reference.md (/model,/resume) · docs/guides/features.md (Session persistence). Version bumped3.5.84→3.5.85inpyproject.toml. Not a breaking change — no runtime behavior changes for existing installs beyond the always-on completion and autosave; publishing the new release (git tag + PyPI) is what lets users pull theprompt_toolkitcore-dependency change viapip install -U/uv tool upgrade. - July 9, 2026: Official Docker image on Docker Hub + a one-command publish script, plus a first-run permission fix. You can now run CheetahClaws without cloning the source:
docker pull chauncygu/cheetahclawsanddocker run --rm -p 8080:8080 chauncygu/cheetahclawsbrings up the Web UI. Three parts shipped. (1) First-runPermissionErrorfixed. The image runs as a non-rootcheetahuser (uid 1000), but theDockerfiledeclaredVOLUME ["/home/cheetah/.cheetahclaws"]and setWORKDIR /workspacewithout pre-creating those directories, so Docker created them root-owned — and the very first launch died withPermissionError: [Errno 13] ... '/home/cheetah/.cheetahclaws/sessions'whenconfig.load_configtried tomkdirthe sessions dir. TheDockerfilenowmkdir -ps both.cheetahclawsand/workspaceandchowns them tocheetahbeforeUSER cheetah, so the anonymous volume and workspace inherit correct ownership and startup succeeds with no host mount required. (2) Compose image is overridable.docker-compose.ymlstill builds and tagscheetahclaws:latestlocally by default, but theimage:key is now${CHEETAH_IMAGE:-cheetahclaws:latest}— setCHEETAH_IMAGE=chauncygu/cheetahclaws:latest docker compose up -dto run the published image and skip the build. (3)scripts/docker-publish.shreads the version frompyproject.toml, tags both:<version>and:latest, and pushes; multi-arch (linux/amd64,linux/arm64) via buildx by default, orSINGLE_ARCH=1for a host-arch-onlydocker build+push, withDRY_RUN=1to preview andPUSH_LATEST=0to skip the floating tag. Docs: docs/guides/docker.md gains a Pull from Docker Hub section (pull/run, compose override, maintainer publish) and an Interactive setup / CLI mode section explaining that the default--webimage configures the model in the Web UI Settings panel, and how to get thepip-style first-run wizard instead (docker run -it … --setup, with the required-v …:/home/cheetah/.cheetahclawsconfig-persistence mount). Current published tags:chauncygu/cheetahclaws:latestand:3.5.84(amd64). Not a breaking change — no source behavior changes; nativepip install cheetahclawsis unaffected. - July 8, 2026:
/workspace— isolated working directories under~/.cheetahclaws/workspaces, now strictly opt-in (PR #162 + follow-up hardening). Adds a/workspaceslash command mirroring the Dulus workflow:list,switch <name>(creates on demand, records last-used),default [name](show/set the startup workspace),create <name>, anddelete <name>(empty dirs only; refuses to delete the workspace you're currently in), plus a bare/workspacethat prints the current workspace + cwd. The follow-up fixes two problems in the original PR before they reach users. (1) No more silentchdiron startup. The merged version unconditionallyos.chdir-ed into~/.cheetahclaws/workspaces/workspace1at every launch — so a user startingcheetahclawsinside their project would have the agent's file tools silently operating on an empty hidden directory instead of their repo. Boot-time switching is now gated behind a newworkspace_autoconfig flag (defaultFalse): out of the box the CLI stays in whatever directory you launched from (unchanged behavior); isolation is enabled explicitly with/config workspace_auto=true. (2)defaultis now sticky.defaultandswitchpreviously shared oneworkspace_lastkey, so setting a startup default was clobbered by the nextswitch. They're now split intoworkspace_default(the sticky startup target, set only by/workspace default) andworkspace_last(most-recent switch); startup precedence isworkspace_default→workspace_last→workspace1. All three keys are registered inconfig.DEFAULTS. Covered bytests/test_workspace_cmd.py(12 tests: CRUD round-trips, current-workspace detection, the distinct-keys regression, startup precedence, and the boot helper). Not a breaking change — withworkspace_autooff (the default), no existing behavior changes. See docs/guides/reference.md (/workspace,workspace_auto).
v3.5.84
- July 6, 2026 (latest, v3.5.84):
/imagegains local-OCR prompt enrichment — clipboard screenshots become actionable text even on non-vision models. The/imagecommand captured a clipboard screenshot and attached it as base64 for a vision model to interpret, which is useless on the dominant local-Ollama setup where the model has no vision at all — the screenshot was silently ignored. Now, after capturing the PNG,/imageruns best-effort local OCR (ocr_image_bytesintools/files.py, wrappingpytesseract) and appends the extracted text to the prompt, clearly labeled as verbatim OCR that may contain recognition errors, so a screenshot of an error dump, a code snippet, a receipt, or a table becomes text the model can act on. Vision models get both signals — pixels and the exact transcribed text (OCR tends to beat vision models at dense text). The helper never raises: missingpytesseract/Pillow/tesseractbinary, corrupt bytes, and empty results all collapse to'', so the feature is a zero-cost no-op when OCR isn't available and never breaks the existing vision path (pending_imagebase64 is still set). Two safety bounds: the appended OCR block is capped at 8000 chars to protect small local context windows, and — because OCR is synchronous (noticeable latency on large images) and injects imperfect text a vision model may not want — the whole enrichment is opt-out viaCHEETAHCLAWS_IMAGE_OCR=0(default on). Covered bytests/test_image_ocr.py(never-raises paths, prompt append, truncation, the still-sets-pending_imageregression, and the env opt-out / default-on toggle). Version bumped3.5.83→3.5.84inpyproject.toml. Not a breaking change — no existing behavior changes; the OCR text is purely additive and disabled with one env var. See docs/guides/features.md (Vision input) · docs/guides/reference.md (/image, env vars). - June 28, 2026: New
accept-editspermission mode — the middle ground betweenautoandaccept-all— plus an exposedplanmode and corrected permission docs. Previouslyautoasked before every file edit whileaccept-allran everything (destructive shell commands included), with no setting in between — awkward for a coding session where you trust the model to edit files but want a prompt before it runs commands.accept-editsfills that gap: it auto-runsWrite/Edit/NotebookEditbut still prompts for any non-allow-listed Bash, so agit push --forceorrmis never run silently (and the host-destroying hard denylist —rm -rf /,mkfs,ddto a raw disk, fork bombs — still applies at execution time, in every mode). Implemented as one branch in_check_permission; reads and Bash keep theautorules. Two cleanups shipped alongside: the already-implementedplanmode (read-only analysis, writes refused except to the plan file) is now listed in the/permissionsmenu and tab-completion instead of being reachable only via/plan; and the system prompt's "Safe vs Unsafe" section — which wrongly told the model thatautoauto-approves checkpoint-protected edits, and that unsafe ops are asked "even underaccept-all" — was rewritten to match the real_check_permissionbehavior (autoprompts for all edits;accept-alldoes not prompt; only the hard denylist blocks unconditionally). README, the 7 i18n READMEs,docs/guides/features.mdanddocs/guides/reference.mdall list the five modes. See docs/guides/reference.md. - June 28, 2026: Memory staleness is now anchored to verification, not file mtime — and the agent is told how to keep memories fresh. Two parts. (1) The bug (PR #150).
MemorySearchrewrites a memory file to bumplast_used_at, which advanced its mtime; both the retrieval recency score and the⚠ stalewarning were derived from that mtime, so a single read of a stale, never-re-verified memory reset its recency to ~1.0 and suppressed its "verify against current code" warning — the "stale-but-confident" failure the design warns against, and worst for the most-retrieved (most-likely-acted-on) memories. The fix adds alast_verifiedfrontmatter field (defaults tocreated); staleness/recency now come fromverified_epoch(last_verified→created→ mtime fallback for legacy files), never raw mtime.touch_last_usedpreserves mtime and never writeslast_verified, so a read can't look like a write. A newMemoryVerifytool /mark_verified()is the only thing that refreshes the clock, called after the agent re-checks the claim against the environment — integral to the fix, since removing the broken implicit refresh (read = fresh) requires providing the correct explicit one. 7 regression tests encode the bug (tests/test_memory_staleness.py); existing memory tests stay green. (2) Follow-up — activating the dormant half. Merging #150 alone left the explicit-refresh path unused: the memory system prompt told the model to verify a memory before acting on it but never mentioned theMemoryVerifytool, so no memory ever got re-verified and still-correct old memories would keep the stale flag and decay in ranking forever. The prompt now explicitly instructs: after confirming a claim still holds, callMemoryVerify(the only thing that clears the flag / restores ranking); if it no longer holds,MemorySave(overwrite) orMemoryDeleteinstead. And the always-injected memory manifest — which still sorted and aged by mtime — is now anchored toverified_epochtoo, matching howMemorySearchranks (legacy files without date fields fall back to mtime, so they're unchanged). See docs/guides/features.md · docs/guides/reference.md.
v3.5.83
- June 23, 2026 (latest) (v3.5.83): Documentation slimmed to its essentials, a native desktop app brought into the repo, and the version-string format unified. Three threads of housekeeping. (1) README / docs trim. The top-level README had grown a verbose multi-paragraph News block and several reference dumps that duplicated the guides. Each News item is now one sentence + a
[Details](docs/news.md)link (the full write-ups stay here); the 59-model Atlas Cloud list moved todocs/guides/usage.md(Option D) leaving only the 3-line example in the README; and the FAQ dropped to its three highest-value entries (MCP, Ollama tool calls, macOS PATH) with the rest pointed atdocs/guides/faq.md. Net README ~551 → ~516 lines with no content lost — everything trimmed already lived indocs/. (2) Native desktop app (desktop/). A thin Electron shell that launchescheetahclaws --web --no-authas a localhost sidecar, parses itsChat UI: http://…/chatready line, and points aBrowserWindowat it — so the production web UI becomes a native window with nothing reimplemented. It couples to the rest only through that CLI contract (verified:npm run smokelaunches the real server against this repo and confirms/chat//healthall serve), andscripts/build-app.shcan freeze the server with PyInstaller into a self-contained.dmg/.exe/.AppImageneeding neither Node nor Python on the user's machine. Surfaced from three entry points (top-of-README callout, Web UI section, Documentation table). Remaining for a shippable installer: code signing / notarization. (3) Version-string format unified. Historical release notes mixedv3.05.xandv3.5.x; all ~166 occurrences across the README, the 7 i18n translations,docs/news.md, the guides, the demo/cast generators, and the recorded.cast/.svgbanners are now the canonicalv3.5.x. Version bumped3.5.82→3.5.83inpyproject.toml. Full suite green (2449 passed, 3 skipped); the desktop sidecar smoke test passes against this repo's web server. Not a breaking change — no runtime behavior changed. - June 16, 2026: All internal modules move into a single
cheetahclawspackage. Previously the importable modules lived flat at the top level (config.py,daemon/,kernel/,mcp_client/,providers.py, …). That works when you run from the repo dir but breaks once CheetahClaws is installed and launched from its entry point: a generic top-level name likeconfigordaemongets shadowed by whatever else is onsys.path— another project'sconfig/directory, the PyPIpython-daemonpackage — andcheetahclawsdies at startup withImportError: cannot import name … from 'config' (unknown location). (An earlier pass that merely dropped acc_prefix from four of these modules re-introduced exactly this collision, which the prefix had originally been added to prevent — so this change supersedes it.) The fix is the standard one: own a single namespace. All 21 single-file modules and 20 sub-packages now live undercheetahclaws/and are imported ascheetahclaws.<name>; the entry scriptcheetahclaws.pybecamecheetahclaws/cli.py, with a deliberately lightcheetahclaws/__init__.py(definesVERSION, lazily proxies CLI entry symbols via PEP 562__getattr__so importing a submodule never drags in the heavy CLI) and acheetahclaws/__main__.pyforpython -m cheetahclaws. Imports were rewritten across all 448.pyfiles — 1269from NAME+ 126import NAME+ 41 dottedimport NAME.substatements, 118 stringpatch/mock/import_moduletargets, subprocess-margv paths, the modular plugin f-string loaders, the voice/video back-compat shims, and embedded driver-script strings — all prefixed withcheetahclaws., using whole-word matching so RPC method names, filenames, and unrelated tokens were left alone.pyproject.tomlnow ships a singlecheetahclaws*package (nopy-modules) with entry pointcheetahclaws.cli:main;agent_templates/moved into the package so it ships as data. Triage of the move surfaced and fixed seven regression classes — kernel/daemon subprocess-margv paths, thetest_packagingimport contract, the voice shim's submodule registration, the daemon e2e launcher, tests that patched the package object instead of theclimodule, tests with hardcoded repo-root data paths, and asys.modulesstub-restore leak betweentest_researchandtest_setup_wizard. Breaking only for code that imports CheetahClaws internals directly —import kernel→from cheetahclaws import kernel,from mcp_client.client import get_mcp_manager→from cheetahclaws.mcp_client.client import get_mcp_manager; thecheetahclawsCLI,python -m cheetahclaws, the Web UI, and all bridges are unaffected. Verified end-to-end:python -m cheetahclaws --versionandfrom cheetahclaws import configboth work from outside the repo (the original crash), a built wheel containscheetahclaws/*with all data files (web, prompts, agent_templates) and zero bare top-level modules, and the full suite is 2449 passed, 3 skipped, 0 failed.
v3.5.82.5
- June 6, 2026 (v3.5.82.5) (latest): macOS install reliably puts
cheetahclawson PATH, and local Ollama models that emit tool calls as text now actually execute them. Two fixes reported in issue #131. (1) Install / PATH on macOS. On macOS the installer creates a dedicated venv (~/.cheetahclaws-venv) andsources it, so the post-install verificationif command -v cheetahclawssucceeded inside the script's own activated shell — it printed "cheetahclaws is on PATH" and short-circuited past the entire rc-file block, including thetouch ~/.zshrcthat was supposed to create the file. Result:~/.zshrcwas never created/updated, and in a fresh terminal (no venv active) the binary was unreachable, so users had to hunt for the install location by hand. The verification step no longer trusts the venv-pollutedcommand -v: it confirms the binary at the expectedBIN_DIR, then (for venv installs) symlinks only thecheetahclawsentry point into~/.local/bin— pipx-style, so the venv'spython/pipnever get prepended to PATH and can't shadow the user's own — creates the right rc file if missing (~/.zshrcfor zsh,~/.bash_profilefor bash on macOS,config.fishfor fish), and appends the exposure dir to PATH there. The fish branch now also writes fish (set -gx PATH …) syntax instead ofexport, and the reload hint points bash-on-macOS at.bash_profile(scripts/install.sh). (2) Ollama tool calls (the "model just keeps talking" bug). The Ollama streaming path (stream_ollama) only read tool calls from Ollama's structuredmessage.tool_callsfield, whereas the OpenAI-compatible cloud path (stream_openai_compat) also recovers tool calls a model emits as text via_find_native_tool_marker+_extract_native_tool_calls. Many local models — Qwen-coder, Gemma, Mistral — emit calls as<tool_call>{…}</tool_call>/<|tool_call|>…/[TOOL_CALLS][…]insidecontent; on the Ollama path that markup was streamed straight to the screen as chat and never executed, so the agent loop saw no tool calls and ended the turn — exactly the reported "tool-calling-style chat that never runs."stream_ollamanow mirrors the cloud path: when a native marker appears in the streamed content it buffers from that point (so the user never sees raw markup), and at end-of-stream parses the buffer into real tool calls (falling back to surfacing the buffered text if parsing fails, so nothing is silently swallowed). Note: Ollama's native/api/chatdoes not accept atool_choiceparameter, so the fix is the text-format recovery, not a request-param change. Existing provider + cache-token suites stay green. See docs/guides/usage.md · docs/guides/faq.md. - June 5, 2026 (v3.5.82): User-controllable token / cost budgets — set a spend cap; on hit the session auto-saves and you can resume or raise it. The quota engine (
quota.py: per-session + per-day token/cost counters, enforced before each model call) already existed but had no friendly surface — you had to know four config keys (session_token_budget/session_cost_budget/daily_token_budget/daily_cost_budget) and there was no way to see how close you were, no warning before the wall, and the hard stop printed a bare[Quota exceeded]. This adds the UX layer on top of the unchanged engine: a/budgetcommand — no args shows usage vs every budget as colored bars + percentages;/budget $5sets a session cost cap (the$means USD),/budget 200ka session token cap (parses200k/1.5m/200000),/budget daily $20//budget daily 2mthe daily caps, and/budget clearremoves all. A--budget $5/--budget 200kstartup flag sets the session cap at launch. Proximity warnings fire at the end of any turn that crosses ≥80% (yellow) / ≥95% (red) of a cap, so the wall never arrives by surprise. On hit the agent now yields aQuotaPauseevent (instead of a plain text line): the REPL auto-saves the session (session_latest.json+ daily backup, the same path/resumereads) and prints a friendly next-steps block — raise the same cap or remove it (/budget clear) then resend, or restart later and/resume. So a long task that runs out of budget is never lost: you analyze, adjust, and continue. Tight enforcement (no surprise overshoot): the check projects the next request's input (compaction.estimate_tokens) and stops before the call if it would cross the cap, and clamps that call'smax_tokensto the remaining headroom (quota.output_room) — so a single tool-heavy turn can't blow 40k→49k past the budget the way a pure "already-spent ≥ limit" check let it. One budget per scope: setting a cap replaces the other unit for that scope (/budget $5after/budget 200kswitches the session cap to cost rather than stacking), so a leftover token cap can't silently keep blocking after you switch to a$cap. Unit-matched hint:QuotaExceeded/QuotaPausecarry which cap broke (key/scope/unit/limit), so the "raise it" suggestion is in the right unit — a token cap shows/budget 40k, a daily cost cap shows/budget daily $40— instead of a generic$amount that wouldn't lift a token cap. New helpersquota.parse_budget/fmt_amount/usage_vs_limits/warnings/output_room; command incommands/core.py:cmd_budget;QuotaPauseinagent.py; REPL handling +--budgetincheetahclaws.py; 42-casetests/test_budget.py(isolated quota dir, incl. a regression that the hint matches the breached unit and that switching units clears the stale cap). The daemon's conservativeserve-mode defaults (200k tok / $2 per session, 2M / $20 per day) are unchanged — interactive stays unlimited by default, the server stays guard-railed. See docs/guides/features.md · docs/guides/reference.md. - June 5, 2026 (v3.5.82): Adaptive Markdown streaming — live output that stays correct on every device. In-place Rich Live redraw is great on capable terminals but breaks elsewhere: it was disabled wholesale over SSH (so SSH users got raw tokens with no formatting), and where it did run it could leave duplicate or stale frames — on macOS Terminal (which can't erase above the scroll boundary), over laggy network PTYs, or with wide CJK / emoji text whose display width a naive line-count gets wrong. The renderer now selects a streaming tier per device in
ui.render.auto_stream_mode(config):live— full in-place redraw, only on terminals known to handle cursor-up (local TTYs, and modern emulators even over SSH: iTerm2, WezTerm, Windows Terminal, VSCode, kitty, Alacritty, Ghostty, detected viaTERM_PROGRAM/TERM/WT_SESSION/KITTY_WINDOW_ID/ALACRITTY_WINDOW_ID/WEZTERM_PANE);commit— append-only progressive Markdown, the safe default for unknown-SSH / Apple Terminal / pipes / non-TTY, where each completed block (split on blank lines, respecting open code fences so a fenced block renders atomically) is rendered and printed permanently and the cursor is never moved, making a duplicate frame structurally impossible regardless of terminal, latency, or character width;plain— raw tokens, only whenrichis unavailable. The append-only floor is provably duplication-free;liveis progressive enhancement on top. Override with/config stream_mode=live|commit|plain(legacy boolean/config rich_live=true|falsestill works →live/commit). Implemented inui/render.py(set_stream_mode/auto_stream_mode/_safe_commit_point/_commit_stream/_commit_flush), wired in at REPL start incheetahclaws.py, with a 26-case test suite intests/test_stream_modes.py(device routing, code-fence-aware block boundaries, append-only commit, and a regression asserting commit mode emits zero cursor sequences even on a TTY with CJK text). Two related UX items shipped alongside:/contextis now a visual grid — a Claude-Code-style 20×10 cell grid of context-window usage, colored and broken down by category (system prompt / system tools / memory files / skills / messages / free space) with per-category token counts and percentages, adapting to the model's real context window and falling back to#/.on non-UTF-8 terminals (commands/core.py:cmd_context); anddeepseek-v4-flashis registered at its 1M context window inproviders._MODEL_CONTEXT_LIMITS(overriding the 128K deepseek provider default, which still applies todeepseek-chat/deepseek-v4-pro), so the prompt%,/context, and the compaction trigger all reflect the true 1M window. See docs/guides/features.md · docs/guides/reference.md.
v3.05.82
- June 5, 2026 (v3.05.82) (latest): User-controllable token / cost budgets — set a spend cap; on hit the session auto-saves and you can resume or raise it. The quota engine (
quota.py: per-session + per-day token/cost counters, enforced before each model call) already existed but had no friendly surface — you had to know four config keys (session_token_budget/session_cost_budget/daily_token_budget/daily_cost_budget) and there was no way to see how close you were, no warning before the wall, and the hard stop printed a bare[Quota exceeded]. This adds the UX layer on top of the unchanged engine: a/budgetcommand — no args shows usage vs every budget as colored bars + percentages;/budget $5sets a session cost cap (the$means USD),/budget 200ka session token cap (parses200k/1.5m/200000),/budget daily $20//budget daily 2mthe daily caps, and/budget clearremoves all. A--budget $5/--budget 200kstartup flag sets the session cap at launch. Proximity warnings fire at the end of any turn that crosses ≥80% (yellow) / ≥95% (red) of a cap, so the wall never arrives by surprise. On hit the agent now yields aQuotaPauseevent (instead of a plain text line): the REPL auto-saves the session (session_latest.json+ daily backup, the same path/resumereads) and prints a friendly next-steps block — raise the same cap or remove it (/budget clear) then resend, or restart later and/resume. So a long task that runs out of budget is never lost: you analyze, adjust, and continue. Tight enforcement (no surprise overshoot): the check projects the next request's input (compaction.estimate_tokens) and stops before the call if it would cross the cap, and clamps that call'smax_tokensto the remaining headroom (quota.output_room) — so a single tool-heavy turn can't blow 40k→49k past the budget the way a pure "already-spent ≥ limit" check let it. One budget per scope: setting a cap replaces the other unit for that scope (/budget $5after/budget 200kswitches the session cap to cost rather than stacking), so a leftover token cap can't silently keep blocking after you switch to a$cap. Unit-matched hint:QuotaExceeded/QuotaPausecarry which cap broke (key/scope/unit/limit), so the "raise it" suggestion is in the right unit — a token cap shows/budget 40k, a daily cost cap shows/budget daily $40— instead of a generic$amount that wouldn't lift a token cap. New helpersquota.parse_budget/fmt_amount/usage_vs_limits/warnings/output_room; command incommands/core.py:cmd_budget;QuotaPauseinagent.py; REPL handling +--budgetincheetahclaws.py; 42-casetests/test_budget.py(isolated quota dir, incl. a regression that the hint matches the breached unit and that switching units clears the stale cap). The daemon's conservativeserve-mode defaults (200k tok / $2 per session, 2M / $20 per day) are unchanged — interactive stays unlimited by default, the server stays guard-railed. See docs/guides/features.md · docs/guides/reference.md. - June 5, 2026 (v3.05.82): Adaptive Markdown streaming — live output that stays correct on every device. In-place Rich Live redraw is great on capable terminals but breaks elsewhere: it was disabled wholesale over SSH (so SSH users got raw tokens with no formatting), and where it did run it could leave duplicate or stale frames — on macOS Terminal (which can't erase above the scroll boundary), over laggy network PTYs, or with wide CJK / emoji text whose display width a naive line-count gets wrong. The renderer now selects a streaming tier per device in
ui.render.auto_stream_mode(config):live— full in-place redraw, only on terminals known to handle cursor-up (local TTYs, and modern emulators even over SSH: iTerm2, WezTerm, Windows Terminal, VSCode, kitty, Alacritty, Ghostty, detected viaTERM_PROGRAM/TERM/WT_SESSION/KITTY_WINDOW_ID/ALACRITTY_WINDOW_ID/WEZTERM_PANE);commit— append-only progressive Markdown, the safe default for unknown-SSH / Apple Terminal / pipes / non-TTY, where each completed block (split on blank lines, respecting open code fences so a fenced block renders atomically) is rendered and printed permanently and the cursor is never moved, making a duplicate frame structurally impossible regardless of terminal, latency, or character width;plain— raw tokens, only whenrichis unavailable. The append-only floor is provably duplication-free;liveis progressive enhancement on top. Override with/config stream_mode=live|commit|plain(legacy boolean/config rich_live=true|falsestill works →live/commit). Implemented inui/render.py(set_stream_mode/auto_stream_mode/_safe_commit_point/_commit_stream/_commit_flush), wired in at REPL start incheetahclaws.py, with a 26-case test suite intests/test_stream_modes.py(device routing, code-fence-aware block boundaries, append-only commit, and a regression asserting commit mode emits zero cursor sequences even on a TTY with CJK text). Two related UX items shipped alongside:/contextis now a visual grid — a Claude-Code-style 20×10 cell grid of context-window usage, colored and broken down by category (system prompt / system tools / memory files / skills / messages / free space) with per-category token counts and percentages, adapting to the model's real context window and falling back to#/.on non-UTF-8 terminals (commands/core.py:cmd_context); anddeepseek-v4-flashis registered at its 1M context window inproviders._MODEL_CONTEXT_LIMITS(overriding the 128K deepseek provider default, which still applies todeepseek-chat/deepseek-v4-pro), so the prompt%,/context, and the compaction trigger all reflect the true 1M window. See docs/guides/features.md · docs/guides/reference.md.
v3.05.81
-
June 4, 2026 (v3.05.81) (latest): Claude-Code-style quiet output — hide tool execution, show one summary line per turn. Long analysis turns used to scroll the terminal with a
⚙ Bash(...)line and a✓ → N lines (… chars)line for every tool call, and the permission prompt dumped the entire inline script (e.g. a 60-linepython3 << 'PYEOF'heredoc). A new quiet mode (on by default) suppresses the per-tool lines — the spinner conveys live activity and a single summary line is emitted at the tool→text boundary, sitting just above the reply (Read 2 files, ran 3 shell commands), the way Claude Code does. Errors and denials still surface so a mid-turn failure is never silent. In quiet mode the permission prompt also collapses a multi-line command to one line (Run: python3 << 'PYEOF' … (+59 行)) instead of printing the whole script./verboseoverrides quiet (full per-tool lines + inputs + token counts); toggle with/quiet, or launch with--show-tools(alias--no-quiet). The startup banner gains anOutput: quiet/Output: fullline so the active mode is visible at a glance. Live status line: the spinner now shows elapsed time plus a running output-token estimate (Thinking… (7s · ↓ 435 tokens)) — char-based, since providers only report real usage at the end — and each quiet turn closes with a real-usage footer✻ Worked for 7.2s · ↑ 1.2k · ↓ 435built from the trueTurnDonecounts. Implemented inui/render.py(turn-level tool accumulator +turn_summary_line(), spinner token meter,print_turn_stats()), wired through the REPL event loop incheetahclaws.py, with the/quiettoggle incommands/config_cmd.py. See docs/guides/features.md. -
June 4, 2026: Context-window override — the prompt % and compaction now follow a settable context length. The prompt's context-usage
%(and the compaction trigger) derive from the model's context window, which previously could only be a hardcoded provider default — andmax_tokens(the OUTPUT cap) doesn't change it, so/config max_tokens=…left the%unchanged (a common point of confusion). New per-session keycontext_window(/config context_window=<N>,0= model default) overrides it, kept deliberately distinct frommax_tokens. A single parser (providers.context_window_override) feeds the prompt%,/context, the compaction trigger, and the per-call output-token cap, so all four stay consistent; it is bidirectional — a smaller value forces earlier compaction, a larger value corrects a stale default. The value is read live each prompt, so switching model orcontext_windowupdates the%with no restart./configwarns when the value exceeds the model's real window (which would disable compaction and let the API reject oversized prompts). No-op when unset, so existing behavior is unchanged. See docs/guides/reference.md. -
June 4, 2026: Rich Live streaming — long responses stay live via a bounded tail window. Large streamed responses that would overflow the terminal's redraw area could leave duplicate or stale frames behind on some emulators (macOS Terminal, etc.), because Rich Live redraws the whole accumulated output in place and the cursor can't reach content that has scrolled into the scrollback. Building on the per-response fallback from PR #133, Rich Live now keeps the live region bounded to the viewport: a short response is shown in full, but once it would overflow, only the last screenful of rendered lines (a tail window) is redrawn — so the Live region can never exceed the terminal and cannot leave stale frames. The complete output is committed once when the response finishes (including on Ctrl-C, since the REPL flushes on interrupt), so the head that scrolled out of the window is never lost. Plain streaming is kept only as a safety net (precise render failed, or the terminal is too small to bound a window). A cheap per-line wrap estimate short-circuits the expensive full
render_lines()measurement while a response stays well under the limit, so normal responses pay no extra Markdown re-render per chunk. Adds focused tests covering full-frame streaming, the full→tail transition, tail-window commit-on-flush, realSegmentsrendering, and both safety-net fallbacks. See docs/guides/features.md. -
May 31, 2026: QQ bot bridge —
/qqconnects cheetahclaws to QQ groups + C2C private chats (PR #121). Uses the officialqq-botpyWebSocket + HTTP SDK (pip install "cheetahclaws[qq]"). botpy's async client runs on a dedicated asyncio event loop inside a daemon thread, bridged to the synchronous main thread via thread-safe queues. Handleson_group_at_message_create(group @-mentions, prefix stripped) andon_c2c_message_create(private). Since QQ has no message-edit API, replies stream as new messages every ~2 s (2000-char chunking) instead of updating a placeholder; passive replies reference the originalmsg_id/event_idwithin QQ's 5-minute window, then fall back to active pushes. Per-target FIFO job queues, slash-command passthrough,!jobs/!retry/!cancelremote control, image input, and permission prompts scoped to the originating chat (no cross-chat approvals). A supervisor reconnects with exponential backoff (2 s → 120 s). Secret handling matches the hardening standard below:$QQ_SECRET(recommended) > REPL arg (deprecated, warns + scrubs history) > config; env-supplied secrets never touch~/.cheetahclaws/config.json./qq <appid>,/qq,/qq stop|status|logout. Two follow-up fixes over the original PR: image downloads moved off the event loop intoloop.run_in_executor(a blockingurlopenwould freeze the WebSocket heartbeat for up to 30 s), and the secret no longer gets written to disk unconditionally. See docs/guides/bridges.md.
v3.05.80
-
May 12, 2026 (v3.05.80): (latest, security-hardening branch): Two-round security hardening sweep — CRITICAL + HIGH findings from the in-repo code review. Lands a cluster of fixes that close real attack surfaces opened by the recent rapid feature growth. Zero regressions across the full 2347-test suite.
Bot tokens off
argv/ readline history.cmd_telegramandcmd_slacknow accept a single-arg form (/telegram <chat_id>//slack <channel_id>) and read the bot token from$TELEGRAM_BOT_TOKEN/$SLACK_BOT_TOKEN. Env-supplied tokens never get persisted to~/.cheetahclaws/config.json; only tokens that actually came in via the deprecated REPL-arg path are saved on disk. Newbridges.scrub_token_from_history(token)walksreadline.get_history_itembackwards and removes any in-memory entry that embeds the token the moment we know its value. Bridge supervisors get atoken=/channel=kwarg so the env-sourced token can flow to the worker thread without ever sitting on the config dict —_slack_start_bridge(config, *, token, channel). Telegram already passed the token explicitly to_tg_supervisor. WeChat is unaffected (QR-scan token, never in argv).Web UI CSRF — double-submit cookie. Server mints
ccsrf=<24B>; Path=/; SameSite=Strict; Max-Age=86400(non-HttpOnly) on every connection that arrives without one._handle_connectiongates POST/PUT/PATCH/DELETE on a matchingX-CSRF-Tokenrequest header (rejection:403 csrf token mismatch). Exempt:/api/auth/{bootstrap,register,login,logout,api/auth}— they establish the session that later carries the cookie. Newweb/static/js/csrf.jsmonkey-patcheswindow.fetchso every state-changing request automatically echoes the cookie value; loaded as the first script inchat.html, the inline terminal script in_build_html, andlab.html. Test harness (tests/test_web_api.py:_client) gains anhttpxevent hook that mirrors the browser behaviour. SameSite=Strict on the JWT cookie remains the first-line defence; CSRF is the second line.Web terminal session ownership.
_PtySession(owner_uid=...)records the creator's JWTsubat/api/sessiontime._check_pty_owner(session, cookie)is consulted at/api/stream//api/input//api/resize— any other authenticated user trying to reach a knownsidgets403 not session owner. Password-only mode (no JWT) keepsowner_uid=Noneand skips the check, preserving the shared-secret model. Closes the trivial-sid-hijack hole in multi-user web deployments.Bash hard-denylist. Eight regexes in
tools/shell.py:_BASH_HARD_DENYrefuse host-destroying patterns regardless ofpermission_mode—rm -rf /and its--recursive/--forcevariants,rm -rf /*,mkfs.*,dd of=/dev/{sd,hd,nvme,vd,mmcblk,xvd},> /dev/{sd,hd,...},chmod -R 777 /,chown -R <user> /, and the classic:(){ :|:& };:fork bomb. Hits the Bash tool, the REPL!cmdescape, and all three bridges'!cmdpaths. Plus NUL-byte + control-char + 64 KB length rejection on every Bash invocation.Filesystem credential denylist.
tools/security.py:_check_path_allowednow refuses access to a small denylist by default — SSH private keys (~/.ssh/id_*),~/.aws,~/.gnupg,~/.kube,~/.docker,~/.netrc,~/.pgpass,/etc/shadow,/etc/gshadow,/etc/sudoers*,/root. Public-by-convention SSH files (config,known_hosts,authorized_keys) remain readable. SetCHEETAHCLAWS_FS_NO_SANDBOX=1to bypass when intentionally auditing your own secrets. Independent ofallowed_root, which still works as the strict-mode toggle for multi-user daemon deployments.Plugin loader hardening. Two new env switches in
plugin/loader.py:CHEETAHCLAWS_DISABLE_PLUGINS=1(kill switch) andCHEETAHCLAWS_PLUGIN_ALLOWLIST=a,b,c(whitelist). EXTERNAL-scope plugins (loaded via$CHEETAHCLAWS_PLUGIN_PATH) print a one-time stderr warning on first load so a stolen env-var-set doesn't silently execute. Module path resolution now usesPath.resolve()+relative_to(install_dir)to confine a malicious manifest's"tools": ["../../etc/passwd_loader"]style entry.MCP env sanitisation.
cc_mcp/client.py:_sanitized_mcp_envstrips a fixed set of process-hijack keys (LD_PRELOAD,LD_LIBRARY_PATH,LD_AUDIT,DYLD_INSERT_LIBRARIES,DYLD_LIBRARY_PATH,PYTHONPATH,PYTHONSTARTUP,PYTHONHOME,PYTHONEXECUTABLE,NODE_OPTIONS,NODE_PATH,BASH_ENV,ENV) from anyenvmap an.mcp.jsonconfig supplies. Dropped keys print a one-line stderr notice. Bypass:CHEETAHCLAWS_MCP_TRUST_ENV=1. Closes a real local-priv-esc path on a host with multiple MCP server configs of varying trust.macOS daemon peer-cred.
cc_daemon/auth.py:get_peer_uidnow branches onsys.platform: Linux keepsSO_PEERCRED, macOS / *BSD goes through ctypes-loadedgetpeereid(2). Closes a long-standing TODO that effectively reduced macOS Unix-socket auth to token-only (a stolen daemon-token implied full RCE without peer-uid validation).Smaller fixes folded in. Web JWT secret loader rewritten with
O_CREAT \| O_EXCL+ 0o600 + post-write mode verification (refuses to read a world-readable secret file; auto-falls-back to in-memory secret if chmod can't be enforced; override withCHEETAHCLAWS_WEB_SECRET). Terminal one-time password fromsecrets.token_urlsafe(6)[:6](~30 bits, online-bruteable) tosecrets.token_urlsafe(32)(~190 bits).cc_config.save_configstripspermission_mode=accept-allbefore persisting — once-confirmed escape hatches no longer outlive the session that set them.session_store.save_sessionwrapped in a module-levelLock+ explicitBEGIN IMMEDIATE/ROLLBACKso two threads writing the samesession_idno longer silently drop one set of changes.agent_runner.pyerr_msginitialised before the try block (defends against aNameErroron first iteration if_handle_permission_requestreturns"error");quota.QuotaExceededmatched byisinstanceinstead of class-name string.compaction.compact_messageswrapsstream_auxiliaryin try/except + falls back to the original messages instead of crashing the agent loop.providers._recover_args_from_textcaps the regex scan window to the last 32 KB of accumulated text (was scanning ~100 KB+ on every tool call).context.get_git_info+get_claude_mdget TTL caches (30 s / 10 s, keyed by cwd) so the per-turngit rev-parse / status / logand CLAUDE.md re-read stop showing up in profiles.cc_mcp/client.pyreader loops usedict.pop()instead ofin+index so a late response after a timeout doesn't race the request side.tool_registry._cache_keyaddssession_iddimension so aRead(/etc/...)cached for one session never leaks to another.session_store.search_sessionsLIKE-fallback path escapes%/_/\before interpolation.Frontend XSS audit. Existing
_esc(textContent-→-innerHTML) and_renderMd(HTML-tag-strip → marked) cover all user/model content paths. One deep-trust hole closed:web/static/js/settings.js:_renderModelspreviously injected server-supplied model names directly into anonclick="app.selectModel('${full}')"attribute — now usesdata-model+ a delegated click handler, so a malicious model registry entry cannot break out of the JS string literal.Defaults you can flip.
CHEETAHCLAWS_BRIDGE_TERMINAL=0hard-disables the bridge!cmdshell entirely (default1, owner-bound bychat_idwhitelist anyway).CHEETAHCLAWS_FS_NO_SANDBOX=1lifts the credential denylist.CHEETAHCLAWS_DISABLE_PLUGINS=1/CHEETAHCLAWS_PLUGIN_ALLOWLIST=…/CHEETAHCLAWS_MCP_TRUST_ENV=1control plugin + MCP behaviour. Full reference in docs/guides/security.md. All 12 CRITICAL + 10 HIGH items from the review now closed (4 of those 22 turned out to be review misjudgements —_all_errorsinit, permission double-answer race,_broadcastiter race, and theQuotaExceededclassname check was a real fix but the surrounding "shell injection in REPL!command" was reclassified as user-typed-input not RCE). Architecture refactor items (cheetahclaws.py/providers.pyGod-object split, sentinel state machine) deliberately left for a separate decision — they're shape changes, not bug fixes. -
May 12, 2026 (
daemon/f-4-followups-f-6-9branch): Daemon foundation roadmap finished — all nine F-1…F-9 items in RFC 0002 now LANDED. Closes the remaining four scope items end-to-end (≈1500 LoC of code + ≈900 LoC of tests + docs). Drilldown:F-4 #2 — Bridge
notifyforwarding. The subprocess-runner reader loop'snotifyIPC branch used to drop the payload on the floor (F-6/7/8 didn't exist yet). Now it routes throughcc_daemon.bridge_supervisor.notify(kind, text). The runner can target a specific bridge viamsg["bridge"](e.g."telegram") or omit it for a"*"broadcast.agent_runner_notifyevents on the bus carry{name, run_id, bridge, delivered, text[:500]}so observers can audit deliveries. Empty-text frames are silently dropped (common during agent shutdown).F-4 #3 — Restart policy. New
RestartPolicydataclass:mode(none|on-crash),max_restarts,backoff_base_s,backoff_cap_s,backoff_jitter_s. Frozen + a purenext_delay(restart_count)so the decision matrix is unit-testable.agent.startaccepts the five fields flat (validation rejectscap < basewhich would clamp every attempt down to a useless ceiling). On a crash the reader'sfinallyarms athreading.Timer(delay, _do_restart, ...); the Timer respawns via a swappable spawner hook (_RESTART_SPAWNERfor tests) and carriesrestart_countforward.stop()cancels the Timer before the kill ladder, and the same_unregister(name, expected=handle)identity check protects against a Timer-fired respawn racing past a deliberate stop. Bus events:agent_runner_restart_scheduled,agent_runner_restart, `agent_runner_restart_faile...
v3.05.79
-
May 10, 2026 (latest, v3.05.79): Web Chat UI session organization + headless-bridges slash handler + stale-session reaper crash fix. Three threads of work merged into a single release. Bridges / headless deploys (#84 follow-up): Telegram / Slack / WeChat
/help,/monitor,/model,/statusproduced zero response in Docker /--webdeploys because_start_headless_bridges()only wiredrun_queryandagent_stateon the sharedsession_ctx— neverhandle_slash. The bridge poll loops gate onif slash_cb:and fell through tocontinuebefore the📩 Telegram:log line, so the failure was invisible indocker compose logs -f. Fix: extracted the slash handler (originally inlined inrepl()) into a module-level factory_make_bridge_slash_handler(state, config, run_query); both REPL and headless paths now use it (single source of truth, no future drift between modes). Stale-session reaper crash:web/api.py:reap_stale_chat_sessions()calledremove_chat_session(sid)without theuser_idthe function now requires for ownership-check parity — every reaper tick raisedTypeError, killing the daemon thread, so staleChatSessionobjects accumulated forever in the in-memory cache. Fix: capture(sid, user_id)pairs from the cachedChatSessionobjects under_chat_lock, then apply outside the lock. Web UI session organization: five-feature bundle layered on top — folders + drag-drop + Move-to context menu, ChatGPT-style active-folder context (click a folder name →+ Newand direct-typing both drop new sessions into that folder, with aChat · in <Folder>topbar breadcrumb), batch select with Select-all-respecting-search-filter, batch delete + combined-Markdown export (chats-N-sessions.md), and a 4-px draggable sidebar divider with localStorage persistence. Backend adds afolderstable,chat_sessions.folder_idnullable FK, in-placePRAGMA table_info+ALTER TABLEmigration ininit_db(), and 5 new HTTP endpoints (GET/POST /api/folders,PATCH/DELETE /api/folders/{id},PATCH /api/sessions/{id}/folder). Also rolled in: issue #111 (handle_slash_sync/handle_slash_streamno longer double-broadcast to WS) and--web --model Xpersistence. Tests: +16 new acrosstest_web_api.py(folder CRUD, batch ops, reaper regression) and the newtest_bridge_slash_handler.py(5 cases pinning the headless handler contract). Full suite: 2154 / 2154 passing, zero regressions. User-side guide:docs/guides/web-ui.md. -
May 10, 2026: Web Chat UI fixes — slash commands no longer reply twice;
--web --model Xactually applies the model. Two related issues that surfaced when wiring a self-hosted vLLM endpoint into the Chat UI. (1) Issue #111 — slash commands duplicated in Chat UI but not in terminal.web/api.py:handle_slash_syncwas both returning events inline in the HTTP response and broadcasting the same events to the WS subscribers of the same client;chat.jsthen iterateddata.eventsAND fired_handleEventfromws.onmessage, rendering every reply twice. Same bug inhandle_slash_streamfor SSE-streamed long commands (/brainstorm,/worker,/agent,/plan). Both helpers now deliver events through a single channel — HTTP/SSE only — so_handleEventruns exactly once per event. Background-thread events (sentinel flows, agent runs) are unaffected: by the time the worker thread emits,_broadcastis already restored to the live WS broadcaster infinally. (2)--web --model Xwas silently ignored. The CLI override branch only ran in the interactive-REPL path; theif args.web:branch loaded config straight from disk and started the server, sopython cheetahclaws.py --web --model custom/qwen2.5-72bwould happily boot but every request handler reloaded~/.cheetahclaws/config.jsonwith the previous model name (e.g.gemma-4-31B-it), producing a confusing404: model does not existagainst the new endpoint. Fix:cheetahclaws.pynow persistsargs.modelto config before callingstart_web_server, matching the documented behavior;provider:model→provider/modelnormalization is identical to the REPL path. User-side guide:docs/guides/web-ui.md(Troubleshooting + Architecture notes updated). -
May 10, 2026: Small-context local models survive large workloads — 4-part fix: ctx cap, auto-fanout, stagnation-stop, output paths under
~/.cheetahclaws/. Repro that motivated the work: running/agent → 1 (Research Assistant)on a 6.6 MB PDF (AutoRedTeamer.pdf— ~70k tokens of extracted text) withcustom/qwen2.5-72b(32k ctx). Old behavior: 400 BadRequest "context length 32768"; the agent_runner kept polling the template every 2 s; the model produced 1500+ identical "task complete" summaries before anything stopped it. New behavior, four cooperating layers: (1) Per-model context-window registry + dynamic max_tokens cap (providers._MODEL_CONTEXT_LIMITS+get_model_context_window+dynamic_cap_max_tokens) — covers Qwen 2.5/3, Llama 3.x, Mistral/Mixtral, Phi, Gemma, DeepSeek local variants;_fetch_custom_model_limitnow backfillsPROVIDERS["custom"]["context_limit"]so compaction sees the live/v1/modelsvalue; per-call shrink based on actual prompt size keepsinput + output + 1024 safety ≤ ctx.compaction.get_context_limitgains an optionalconfigarg so custom-endpoint detection works on the very first turn. (2) Auto-fanout for oversize tool outputs (multi_agent/fanout.py) — when a single tool result (Read on a huge PDF, Grep over a giant tree, WebFetch of a long article) exceeds 0.4 × ctx_window, split into chunks at paragraph boundaries with token-overlap, dispatch parallel sub-LLM map calls (one per chunk, default cap 5 subagents), merge with a single reduce call; substitutes the merged summary in conversation history instead of letting the next API call overflow. Hooked at the tool-result append site inagent.py; transparent UX prints[Auto-fanout: <Tool> returned ~N chars (>threshold) → dispatching K parallel sub-summaries]. Configurable:auto_fanout_enabled/_threshold/_max_subagents/_chunk_overlap_tokens. (3) Stagnation-stop inagent_runner.py— when the model emits the same summary N iterations in a row (default 3, whitespace/case-normalized), stop the loop with a clear notification instead of burning thousands of API calls; configurable viaauto_agent_dup_summary_limit(0 disables). (4) Agent output paths under~/.cheetahclaws/—/agentwizard now resolves relative output filenames (e.g.research_notes.md) to absolute paths under~/.cheetahclaws/agents/<name>/output/instead of CWD;AgentRunnerexposesrunner.output_dir, eagerly mkdir'd; Summary block + post-start info show the resolved path in green; absolute paths pass through unchanged. Tests: +47 new (fanout 23, ctx cap 18, dup-stop 13, output paths 8). Full suite: 2139 passing, zero regressions. User-side guide:docs/guides/extensions.md. -
May 9, 2026: Read tool auto-redirects on overflow — defense-in-depth for the case where model ignores the template instruction. Re-running the same
/agent + autodan.pdffailure showed two real-world problems with the prior fix: (1) The user was running the pip-installed binary (/home/shangdinggu/anaconda3/bin/cheetahclaws), not the source tree. New tools / templates added to source had no effect. (2) Even if the user reinstalled, qwen2.5-72b would likely still callReadinstead ofSummarizeLargeFile— models default to familiar tools no matter what the template says. The fix moves the routing decision into the Read tool itself. (a) New_maybe_redirect_to_summarizehelper (tools/files.py). WhenReadorReadPDFwould return content too large to safely fit in the next API call, it instead returns a short redirect message like[ReadTooLarge: file is too large — call SummarizeLargeFile with file_path='X' instead] PREVIEW: …. The model sees the redirect, callsSummarizeLargeFile, gets a chunked-and-merged summary back. The raw content never enters the API call. (b) CJK-aware token estimation. CJK content tokenizes at ~1 token per character (vs ~2.8 chars/token for English). New_is_cjk_heavy()heuristic: ≥20% CJK characters → use 1:1 char-to-token estimate. A 24K-char Chinese file is 24K tokens, not 8.6K, and now triggers redirect on a 32K-context model. (c) Conservative ceiling for unreliable provider declarations.custom/<model>provider declares 128K context by default but the underlying model is often 32K (qwen2.5-72b, llama 3 8B, etc.). Newsafe_ctx = min(declared_ctx, 30000)caps the threshold at 30K tokens regardless of provider claims — the redirect now fires on the user's exact ~25K-token PDF case (would NOT have fired with the unconditional 128K ceiling, which is exactly the bug). (d) Wrapped Read registration (tools/__init__.py). New_read_with_overflow_checklambda calls_maybe_redirect_to_summarizeafter_readreturns; for results <8KB it skips (not worth the check). ReadPDF gets the same treatment inline in_read_pdf. Why this works even on the old install: as soon as the user updatestools/files.pyandtools/__init__.py, the redirect fires regardless of whether SummarizeLargeFile / template changes are present. The redirect's prose tells the model exactly which tool to call and with what args. Tests: 14 new pytest cases (tests/test_read_overflow_redirect.py) — CJK detection (English / Chinese / Japanese / mixed-minority / empty), threshold logic (small file → no redirect; user's exact failure case → redirect with right pointer; CJK at lower char count triggers vs same chars in English; conservative ceiling protects against overconfident provider; preview included for context). Plus 2 integration tests viaexecute_tool("Read", ...)confirming the wrapper applies the redirect ...
v3.05.78
-
May 8, 2026 (v3.05.78): May 8, 2026: F-2/F-3 follow-ups + CI unblock (
feature/fix-f2). Main has been red since9c01237d(the trading-agent #99 merge) becausetests/test_packaging.py::test_required_module_imports[modular.trading.ml](issue #97 regression test) caught thatmodular/trading/ml/features.pyandmodular/trading/portfolio.pyimport numpy at module top while numpy is in the[trading]extra —pip install .shipped a broken wheel and #100 / #101 inherited the red. Two-commit fix on top of #101: (a)fix(ci)— drop the dead numpy import fromfeatures.py; defer numpy to insidestacker.py:train()/predict_proba()past their early-return paths; gateportfolio.py's numpy behindtry/except; addpytest.mark.skipifon the optimizer / managed-portfolio / ML-training / factor-scan tests so lean-install CI skips them cleanly. Verified: clean venv with only[web,autosuggest](the exact CI install) 1075 passed, 11 skipped; with full extras 1086 passed, no regressions. (b)fix(daemon)— five F-2/F-3 follow-ups: movemonitor.scheduler.start(...)past the listener bind incc_daemon/cli.py:cmd_serve(so a misconfigured fetch/deliver can't fail before the daemon is reachable); add_foreign_daemon_running()step-aside check at every scheduler loop tick to close the race where REPL/monitor startfires before the daemon writes its discovery file (both schedulers would otherwise race onlast_run_at); flipcc_daemon/schema.pytoPRAGMA synchronous=NORMAL(safe under WAL, 8× fasterEventBus.publish— 305 μs/event → 39 μs/event, important for streaming agent output); clarify injobs.py/monitor/store.py/docs/architecture.mdthat the JSON→SQLite migration is one-way (PR #101's wording implied a fallback read path that doesn't exist); updatedocs/RFC/0002-daemon-foundation-roadmap.mdF-2/F-3 status from OPEN → MERGED. Branch:feature/fix-f2. -
Research lab Phase A — autonomous multi-day research; WeChat smart-reply +
/draftsemi-auto reply; reliability + UX hardening across the lab pipeline. Two big surfaces shipped together: (a) the research lab is no longer single-shot —/lab resume <run_id> [<stage>]reconstructsLabStatefrom SQLite to continue or rewind a run;/lab iterate <run_id>runs a 3-reviewer self-review on the final report (novelty / rigor / clarity / evidence, 1-10), routes the lowest-scoring dimension to the corresponding stage (novelty→QUESTIONING, rigor→IMPLEMENTATION, clarity→DRAFTING, evidence→EXPERIMENT), rewinds + re-runs, loops untiltarget_score/max_iterations/ plateau / budget;/lab backlog add <topic> --iterate --target=N --max=N --prio=Nqueues many topics,/lab daemon startruns them 24/7 in a single-worker loop with crash-recovery (reset_running_backlogunsticks stale rows on next start);/lab modelsprints the effective per-role model + which API key drove each pick + warns when reviewers span <N families (homogeneous review = no meta-loop signal);/lab migrate-paths [--apply]renames legacylab_xxx/output dirs to the human-readable<date>_<time>_<topic-slug>_<run_id_short>form (e.g.2026-05-08_14-30_post-transformer-architectures-survey_b16036de/). (b) WeChat smart-reply panel — when a whitelisted contact sends an inbound message, an auxiliary cheap model drafts 3 candidate replies and pushes them as a panel to yourfilehelper(文件传输助手); reply with1/2/3/AA 1to send, freeform text to customise,xto skip,qfor queue. SQLite-persisted at~/.cheetahclaws/wx_smart_reply.db(in-memory fallback on init failure); contacts JSON at~/.cheetahclaws/wx_contacts.jsonis mtime-hot-reloaded; bot-owner self-uid is auto-recorded on first inbound and excluded from smart-reply unconditionally, so your own messages always reach the agent regardless of whitelist contents. (c)/draft <message>slash command — semi-automatic reply suggestion path for cases where the bot can't intercept the inbound directly (bot account ≠ user main account on iLink ClawBot). 3 candidates drafted via the auxiliary model, optionally tone-conditioned via@<contact_uid_or_label>againstwx_contacts.json; when invoked from a bridge channel (WeChat / Telegram / Slack), candidates are also echoed back to the originating uid + stashed inbridges.draft_cacheso a digit-only reply (1/2/3) consumes the chosen text one-shot, no agent invocation, no smart-reply panel triggered. Reliability hardening on top of #88's MCP work:research/http.pynow uses 429-aware backoff (10/30/60/120s vs 0.5/1/2/4s for 5xx) and honoursRetry-Afterheaders (capped at 180s); the lab surveyor stage grounds in realresearch.aggregator.research()hits before invoking the LLM (top-30 academic+tech results passed as context, persisted assurvey_search_hitsartifact for replay) — fabricated-citation rate drops sharply on tested topics;_dedupe_self_repeat()trims cheap-model degenerate sampling (text == text+text) before storage so reviewer prompts don't see doubled inputs;_extract_numbereddedupes by content (questioner emitting1..5\n1..5keeps 5, not 10); the citation verifier now has a per-citation 30sconcurrent.futureshard wall-clock (kills slow-loris sockets that urllib's socket-timeout ignores) + a 5-min stage-level cap with progress callbacks surfaced to/lab logs(the 11-min hang we saw in the field is gone). REPL ergonomics:/lab daemon startand/lab startnow print the eventual report.md path up front + live-stream stage transitions to the terminal as they happen;/lab status <run_id>shows both new + legacy paths so the user can find old reports too;/configparses JSON-style values (lists, dicts, signed numbers, quoted strings) —/config wechat_smart_reply_whitelist=["wxid_..."]no longer silently saved as a literal string; leading whitespace before/is now stripped before slash-dispatch (so a paste with a stray space still hits the dispatcher, not the agent). Tests: 884 passing (842 unit/integration + 22 e2e), zero regressions; ~80 new pytest cases covering iteration scoring, state reconstruction, backlog atomicity, verifier hard-timeout, slug edge cases, dedupe patterns, self-uid bypass.**
v3.05.77
- May 7, 2026 (v3.05.77): MCP HTTP/SSE transport + OAuth 2.0 PKCE,
.envloader,ANTHROPIC_ENDPOINTcorporate-proxy override, AskUserQuestion UI polish (#88, #89) —cc_mcp/client.pynow speaks Streamable HTTP (POST →text/event-streamreply) in addition to stdio and pure SSE, with theAccept: application/json, text/event-streamheader servers like sap-jira require to stop 406-ing. OAuth 2.0: newcc_mcp/oauth.pyimplements the full MCP Authorization spec — RFC 9728 resource-server discovery → RFC 8414 AS metadata → RFC 7591 dynamic client registration → Authorization Code + PKCE (S256) flow with browser redirect → automatic refresh-token rotation. Tokens persist atomically to~/.cheetahclaws/mcp_oauth.jsonat mode0600with the parent directory locked to0700. The redirect-URI port is picked once and reused for both registration and the local callback server, the OAuth scope is sourced from the AS's advertisedscopes_supported(preferringmcpif listed, otherwise the first one, otherwise omitted entirely so servers without anmcpscope no longer reject withinvalid_scope), and_ensure_oauth()is guarded by a dedicated lock so concurrent 401-retries can't race on the httpx client rebuild. REPL:/mcp add <name> --transport http <url>and/mcp add <name> --transport sse <url>for one-line HTTP server registration; explicit/mcp listsubcommand with full-width tool descriptions wrapped at 72 cols. Server name sanitization: hyphenated names likegithub-toolsnow resolve correctly through themcp__server__toolqualified-name path..envloader:_load_env()runs at the very top ofcheetahclaws.pybefore any other import readsos.environ, so.envkeys are visible to every module without losing existing-shell-var precedence (os.environ.setdefault). MCP HTTPheadersvalues are passed throughos.path.expandvars, so"Authorization": "Bearer $GITHUB_TOKEN"works out of the box.ANTHROPIC_ENDPOINTenv var (also reachable via.env) overrides the persistedanthropic_endpointconfig and is used by both the streaming Anthropic client (providers.pypassesbase_url=...toanthropic.Anthropic) and the connectivity probes in/doctor/ setup wizard, letting corporate proxies swapapi.anthropic.comcleanly. UI:AskUserQuestionis auto-approved alongsideEnterPlanMode/ExitPlanMode(it's an interactive tool by definition, a permission prompt was redundant), the spinner and result line are suppressed inprint_tool_start/end, the question text is rendered throughclr()with Markdown stripped (**bold**,`code`,*italic*), and option indices/descriptions are colorized. The REPL prompt now prints a full-width─rule viaos.get_terminal_size()(80-char fallback) before each input, matching Claude Code's visual rhythm.** - May 5, 2026: Telegram bridge file round-trip + cross-channel pickable permission prompts (#84) —
bridges/telegram.pypreviously only had_tg_send(text viasendMessage), so when the model claimed it had "sent a file" it was just text and the[approve][reject]text in permission prompts only looked like buttons. Added_tg_send_document(multipart/form-data upload, 49 MB cap with explicit oversize/empty/missing/network/API-rejection error reporting), an inbounddocumenthandler that saves uploads to/workspace(ortempfile.gettempdir()outside Docker) with sanitized filenames and a path-aware prompt, a!sendfile <path>user command for explicit on-demand sends, and an auto-send hook in_bg_runnerthat mails any file written by theWritetool — FIFO-paired with the in-flightfile_path, skipped onError:/Denied:results, and de-duplicated per turn so parallel writes don't double-mail. Cross-channel permission UX:ask_input_interactive(options=[(label, value), …])now renders an interactive picker on every bridge — Telegram gets a realinline_keyboard(callback_data="cc:<prompt_id>:<value>",_handle_callback_querydoes auth + stale-prompt-id drop +answerCallbackQuery+editMessageText "✓ Selected: y"), Slack and WeChat get a numbered menu in the message body (reply with digit / canonical letter / label word — all resolve via_resolve_choice), terminal prints the same numbered menu before the input cursor;ask_permission_interactivepasses[(✅ Approve, y), (❌ Reject, n), (✅✅ Accept all, a)]. Backward-compatible: every existingask_input_interactivecall site (nooptions=) keeps free-text behavior. 49 new pytest cases (tests/test_telegram_bridge.py+tests/test_options_menu.py) — no real network calls. 718 passed, zero regressions on the 669 pre-existing.--accept-allwas a red herring; the bridge simply lacked the upload code path. - May 2, 2026: Docker chat UI assets 404 follow-up (#73) —
web/server.pynow resolves_WEB_DIRviaimportlib.resources.files("web")instead ofPath(__file__).parent, so static files are found whether the package is installed editable or non-editable. The dotfile guard in the static-file branch now only inspects path segments inside_WEB_DIR, so installs sitting under.venv/,.local/, etc. no longer 404 every asset.[tool.setuptools.package-data]forwebwidened tostatic/**/*so non-editable wheels reliably ship the fullweb/static/subtree. Plus a newdocs/guides/docker.md"Custom Dockerfile pitfalls" section covering the editable-install requirement and the most common 404 root cause for users rolling their own image. - Apr 30, 2026: Docker / home-server support (#73) —
Dockerfile,docker-compose.yml,.env.example, host Ollama viahost.docker.internal, workspace bind-mount for Samba sharing.--webmode now auto-starts configured Telegram / WeChat / Slack bridges in the same process so a single container delivers browser UI + phone bridge. Plus two terminal/agent fixes:AskUserQuestionno longer deadlocks the terminal (#69) — synchronous render+read instead of a queue/event the agent thread can't drain.messages_to_openaiemitscontent: ""instead ofnullfor tool-only assistant turns so Ollama's OpenAI-compat endpoint stops 400-ing withinvalid message content type: <nil>; 400 /BadRequestErrorreclassified as a non-retryableINVALID_REQUESTso a malformed body no longer trips the circuit breaker (#71). - Apr 24, 2026: Support Deepseek V4 models, multi-model prompt adaptation — single shared
default.mdbaseline + tiny per-family overlays (Anthropic XML tags · Gemini 3 explicit Agentic Mode · OpenAI o-series no-narration). Routing is by model family, not provider/runtime — same Qwen prompt whether served via DashScope, Ollama, or OpenRouter. Overlays must cite a vendor prompting guide (≤ 20 lines, enforced by tests). DeepSeek v4 thinking-mode protocol (reasoning_contentround-trip +thinking: ONby default). fix(setup-wizard): tolerate api_key_env=None for ollama/lmstudio (#59)