Skip to content

Releases: SafeRL-Lab/cheetahclaws

v3.5.85

Choose a tag to compare

@chauncygu chauncygu released this 11 Jul 03:09
1dd75ac
  • July 10, 2026 (v3.5.85): REPL quality-of-life — completion works on every install, /model gets a Tab picker, and sessions autosave every turn. Three related changes. (1) prompt_toolkit is now a core dependency. The typing-time completion menu (slash commands, subcommands, the new /model picker) is driven by prompt_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 clean pip install cheetahclaws or an isolated uv tool install cheetahclaws fell back to bare readline (Tab-only, no live dropdown). Since the interactive REPL is the product, prompt_toolkit>=3.0.43 moved from [project.optional-dependencies].autosuggest into [project].dependencies (and into the core block of requirements.txt), so every install method now gets live completion out of the box. The readline fallback path in ui/input.py is untouched — it still covers any environment where prompt_toolkit genuinely can't be installed. The [autosuggest] extra is kept as a harmless no-op alias for backward compatibility. (2) /model dynamic completion (PR #166). Typing /model and pressing Tab now offers a provider/model picker — one default model per configured provider plus a two-level litellm/<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/g narrows to OpenAI models; litellm/openrouter/ expands that backend). Wired into both the prompt_toolkit and readline completers via a new dynamic-completions registry; the litellm provider 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 to session_latest.json only 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 /remember writes were already immediate, so only the transcript was at risk). A new autosave_session() (in commands/session.py) is now called at the end of every turn in run_query: it rewrites only session_latest.json via a temp file + flush() + os.fsync() + atomic os.replace() (durable against a power cut, and a crash can never leave a half-written file), stays silent (no console spam), reuses one stable session_id so each turn overwrites the same file, and is best-effort (never raises into the REPL). It deliberately does not write a daily/ copy, append to history.json, or touch SQLite — those remain exit-time finalization steps in save_latest(), which still prints the loud Session saved → … paths on quit. Net effect: /resume now 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 bumped 3.5.843.5.85 in pyproject.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 the prompt_toolkit core-dependency change via pip 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/cheetahclaws and docker run --rm -p 8080:8080 chauncygu/cheetahclaws brings up the Web UI. Three parts shipped. (1) First-run PermissionError fixed. The image runs as a non-root cheetah user (uid 1000), but the Dockerfile declared VOLUME ["/home/cheetah/.cheetahclaws"] and set WORKDIR /workspace without pre-creating those directories, so Docker created them root-owned — and the very first launch died with PermissionError: [Errno 13] ... '/home/cheetah/.cheetahclaws/sessions' when config.load_config tried to mkdir the sessions dir. The Dockerfile now mkdir -ps both .cheetahclaws and /workspace and chowns them to cheetah before USER 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.yml still builds and tags cheetahclaws:latest locally by default, but the image: key is now ${CHEETAH_IMAGE:-cheetahclaws:latest} — set CHEETAH_IMAGE=chauncygu/cheetahclaws:latest docker compose up -d to run the published image and skip the build. (3) scripts/docker-publish.sh reads the version from pyproject.toml, tags both :<version> and :latest, and pushes; multi-arch (linux/amd64,linux/arm64) via buildx by default, or SINGLE_ARCH=1 for a host-arch-only docker build+push, with DRY_RUN=1 to preview and PUSH_LATEST=0 to 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 --web image configures the model in the Web UI Settings panel, and how to get the pip-style first-run wizard instead (docker run -it … --setup, with the required -v …:/home/cheetah/.cheetahclaws config-persistence mount). Current published tags: chauncygu/cheetahclaws:latest and :3.5.84 (amd64). Not a breaking change — no source behavior changes; native pip install cheetahclaws is unaffected.
  • July 8, 2026: /workspace — isolated working directories under ~/.cheetahclaws/workspaces, now strictly opt-in (PR #162 + follow-up hardening). Adds a /workspace slash command mirroring the Dulus workflow: list, switch <name> (creates on demand, records last-used), default [name] (show/set the startup workspace), create <name>, and delete <name> (empty dirs only; refuses to delete the workspace you're currently in), plus a bare /workspace that prints the current workspace + cwd. The follow-up fixes two problems in the original PR before they reach users. (1) No more silent chdir on startup. The merged version unconditionally os.chdir-ed into ~/.cheetahclaws/workspaces/workspace1 at every launch — so a user starting cheetahclaws inside 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 new workspace_auto config flag (default False): 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) default is now sticky. default and switch previously shared one workspace_last key, so setting a startup default was clobbered by the next switch. They're now split into workspace_default (the sticky startup target, set only by /workspace default) and workspace_last (most-recent switch); startup precedence is workspace_defaultworkspace_lastworkspace1. All three keys are registered in config.DEFAULTS. Covered by tests/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 — with workspace_auto off (the default), no existing behavior changes. See docs/guides/reference.md (/workspace, workspace_auto).

v3.5.84

Choose a tag to compare

@chauncygu chauncygu released this 07 Jul 04:52
f72d4e2
  • July 6, 2026 (latest, v3.5.84): /image gains local-OCR prompt enrichment — clipboard screenshots become actionable text even on non-vision models. The /image command 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, /image runs best-effort local OCR (ocr_image_bytes in tools/files.py, wrapping pytesseract) 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: missing pytesseract/Pillow/tesseract binary, 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_image base64 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 via CHEETAHCLAWS_IMAGE_OCR=0 (default on). Covered by tests/test_image_ocr.py (never-raises paths, prompt append, truncation, the still-sets-pending_image regression, and the env opt-out / default-on toggle). Version bumped 3.5.833.5.84 in pyproject.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-edits permission mode — the middle ground between auto and accept-all — plus an exposed plan mode and corrected permission docs. Previously auto asked before every file edit while accept-all ran 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-edits fills that gap: it auto-runs Write/Edit/NotebookEdit but still prompts for any non-allow-listed Bash, so a git push --force or rm is never run silently (and the host-destroying hard denylist — rm -rf /, mkfs, dd to a raw disk, fork bombs — still applies at execution time, in every mode). Implemented as one branch in _check_permission; reads and Bash keep the auto rules. Two cleanups shipped alongside: the already-implemented plan mode (read-only analysis, writes refused except to the plan file) is now listed in the /permissions menu and tab-completion instead of being reachable only via /plan; and the system prompt's "Safe vs Unsafe" section — which wrongly told the model that auto auto-approves checkpoint-protected edits, and that unsafe ops are asked "even under accept-all" — was rewritten to match the real _check_permission behavior (auto prompts for all edits; accept-all does not prompt; only the hard denylist blocks unconditionally). README, the 7 i18n READMEs, docs/guides/features.md and docs/guides/reference.md all 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). MemorySearch rewrites a memory file to bump last_used_at, which advanced its mtime; both the retrieval recency score and the ⚠ stale warning 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 a last_verified frontmatter field (defaults to created); staleness/recency now come from verified_epoch (last_verifiedcreated → mtime fallback for legacy files), never raw mtime. touch_last_used preserves mtime and never writes last_verified, so a read can't look like a write. A new MemoryVerify tool / 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 the MemoryVerify tool, 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, call MemoryVerify (the only thing that clears the flag / restores ranking); if it no longer holds, MemorySave (overwrite) or MemoryDelete instead. And the always-injected memory manifest — which still sorted and aged by mtime — is now anchored to verified_epoch too, matching how MemorySearch ranks (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

Choose a tag to compare

@chauncygu chauncygu released this 23 Jun 17:41
c5f61f2
  • 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 to docs/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 at docs/guides/faq.md. Net README ~551 → ~516 lines with no content lost — everything trimmed already lived in docs/. (2) Native desktop app (desktop/). A thin Electron shell that launches cheetahclaws --web --no-auth as a localhost sidecar, parses its Chat UI: http://…/chat ready line, and points a BrowserWindow at 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 smoke launches the real server against this repo and confirms /chat / /health all serve), and scripts/build-app.sh can freeze the server with PyInstaller into a self-contained .dmg/.exe/.AppImage needing 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 mixed v3.05.x and v3.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/.svg banners are now the canonical v3.5.x. Version bumped 3.5.823.5.83 in pyproject.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 cheetahclaws package. 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 like config or daemon gets shadowed by whatever else is on sys.path — another project's config/ directory, the PyPI python-daemon package — and cheetahclaws dies at startup with ImportError: cannot import name … from 'config' (unknown location). (An earlier pass that merely dropped a cc_ 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 under cheetahclaws/ and are imported as cheetahclaws.<name>; the entry script cheetahclaws.py became cheetahclaws/cli.py, with a deliberately light cheetahclaws/__init__.py (defines VERSION, lazily proxies CLI entry symbols via PEP 562 __getattr__ so importing a submodule never drags in the heavy CLI) and a cheetahclaws/__main__.py for python -m cheetahclaws. Imports were rewritten across all 448 .py files — 1269 from NAME + 126 import NAME + 41 dotted import NAME.sub statements, 118 string patch / mock / import_module targets, subprocess -m argv paths, the modular plugin f-string loaders, the voice/video back-compat shims, and embedded driver-script strings — all prefixed with cheetahclaws., using whole-word matching so RPC method names, filenames, and unrelated tokens were left alone. pyproject.toml now ships a single cheetahclaws* package (no py-modules) with entry point cheetahclaws.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 -m argv paths, the test_packaging import contract, the voice shim's submodule registration, the daemon e2e launcher, tests that patched the package object instead of the cli module, tests with hardcoded repo-root data paths, and a sys.modules stub-restore leak between test_research and test_setup_wizard. Breaking only for code that imports CheetahClaws internals directlyimport kernelfrom cheetahclaws import kernel, from mcp_client.client import get_mcp_managerfrom cheetahclaws.mcp_client.client import get_mcp_manager; the cheetahclaws CLI, python -m cheetahclaws, the Web UI, and all bridges are unaffected. Verified end-to-end: python -m cheetahclaws --version and from cheetahclaws import config both work from outside the repo (the original crash), a built wheel contains cheetahclaws/* 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

Choose a tag to compare

@chauncygu chauncygu released this 16 Jun 16:30
e33dd49
  • June 6, 2026 (v3.5.82.5) (latest): macOS install reliably puts cheetahclaws on 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) and sources it, so the post-install verification if command -v cheetahclaws succeeded inside the script's own activated shell — it printed "cheetahclaws is on PATH" and short-circuited past the entire rc-file block, including the touch ~/.zshrc that was supposed to create the file. Result: ~/.zshrc was 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-polluted command -v: it confirms the binary at the expected BIN_DIR, then (for venv installs) symlinks only the cheetahclaws entry point into ~/.local/bin — pipx-style, so the venv's python/pip never get prepended to PATH and can't shadow the user's own — creates the right rc file if missing (~/.zshrc for zsh, ~/.bash_profile for bash on macOS, config.fish for fish), and appends the exposure dir to PATH there. The fish branch now also writes fish (set -gx PATH …) syntax instead of export, 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 structured message.tool_calls field, 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][…] inside content; 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_ollama now 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/chat does not accept a tool_choice parameter, 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 /budget command — no args shows usage vs every budget as colored bars + percentages; /budget $5 sets a session cost cap (the $ means USD), /budget 200k a session token cap (parses 200k / 1.5m / 200000), /budget daily $20 / /budget daily 2m the daily caps, and /budget clear removes all. A --budget $5 / --budget 200k startup 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 a QuotaPause event (instead of a plain text line): the REPL auto-saves the session (session_latest.json + daily backup, the same path /resume reads) 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's max_tokens to 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 $5 after /budget 200k switches 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 / QuotaPause carry 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 helpers quota.parse_budget / fmt_amount / usage_vs_limits / warnings / output_room; command in commands/core.py:cmd_budget; QuotaPause in agent.py; REPL handling + --budget in cheetahclaws.py; 42-case tests/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 conservative serve-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 via TERM_PROGRAM / TERM / WT_SESSION / KITTY_WINDOW_ID / ALACRITTY_WINDOW_ID / WEZTERM_PANE); commitappend-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 when rich is unavailable. The append-only floor is provably duplication-free; live is progressive enhancement on top. Override with /config stream_mode=live|commit|plain (legacy boolean /config rich_live=true|false still works → live/commit). Implemented in ui/render.py (set_stream_mode / auto_stream_mode / _safe_commit_point / _commit_stream / _commit_flush), wired in at REPL start in cheetahclaws.py, with a 26-case test suite in tests/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: /context is 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); and deepseek-v4-flash is registered at its 1M context window in providers._MODEL_CONTEXT_LIMITS (overriding the 128K deepseek provider default, which still applies to deepseek-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

Choose a tag to compare

@chauncygu chauncygu released this 07 Jun 05:58
99a3b4a
  • 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 /budget command — no args shows usage vs every budget as colored bars + percentages; /budget $5 sets a session cost cap (the $ means USD), /budget 200k a session token cap (parses 200k / 1.5m / 200000), /budget daily $20 / /budget daily 2m the daily caps, and /budget clear removes all. A --budget $5 / --budget 200k startup 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 a QuotaPause event (instead of a plain text line): the REPL auto-saves the session (session_latest.json + daily backup, the same path /resume reads) 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's max_tokens to 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 $5 after /budget 200k switches 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 / QuotaPause carry 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 helpers quota.parse_budget / fmt_amount / usage_vs_limits / warnings / output_room; command in commands/core.py:cmd_budget; QuotaPause in agent.py; REPL handling + --budget in cheetahclaws.py; 42-case tests/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 conservative serve-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 via TERM_PROGRAM / TERM / WT_SESSION / KITTY_WINDOW_ID / ALACRITTY_WINDOW_ID / WEZTERM_PANE); commitappend-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 when rich is unavailable. The append-only floor is provably duplication-free; live is progressive enhancement on top. Override with /config stream_mode=live|commit|plain (legacy boolean /config rich_live=true|false still works → live/commit). Implemented in ui/render.py (set_stream_mode / auto_stream_mode / _safe_commit_point / _commit_stream / _commit_flush), wired in at REPL start in cheetahclaws.py, with a 26-case test suite in tests/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: /context is 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); and deepseek-v4-flash is registered at its 1M context window in providers._MODEL_CONTEXT_LIMITS (overriding the 128K deepseek provider default, which still applies to deepseek-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

Choose a tag to compare

@chauncygu chauncygu released this 05 Jun 05:59
cb282bf
  • 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-line python3 << '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. /verbose overrides quiet (full per-tool lines + inputs + token counts); toggle with /quiet, or launch with --show-tools (alias --no-quiet). The startup banner gains an Output: quiet / Output: full line 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 · ↓ 435 built from the true TurnDone counts. Implemented in ui/render.py (turn-level tool accumulator + turn_summary_line(), spinner token meter, print_turn_stats()), wired through the REPL event loop in cheetahclaws.py, with the /quiet toggle in commands/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 — and max_tokens (the OUTPUT cap) doesn't change it, so /config max_tokens=… left the % unchanged (a common point of confusion). New per-session key context_window (/config context_window=<N>, 0 = model default) overrides it, kept deliberately distinct from max_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 or context_window updates the % with no restart. /config warns 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, real Segments rendering, and both safety-net fallbacks. See docs/guides/features.md.

  • May 31, 2026: QQ bot bridge — /qq connects cheetahclaws to QQ groups + C2C private chats (PR #121). Uses the official qq-botpy WebSocket + 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. Handles on_group_at_message_create (group @-mentions, prefix stripped) and on_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 original msg_id/event_id within QQ's 5-minute window, then fall back to active pushes. Per-target FIFO job queues, slash-command passthrough, !jobs/!retry/!cancel remote 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 into loop.run_in_executor (a blocking urlopen would 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

Choose a tag to compare

@chauncygu chauncygu released this 31 May 04:48
60e1eff
  • 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_telegram and cmd_slack now 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. New bridges.scrub_token_from_history(token) walks readline.get_history_item backwards and removes any in-memory entry that embeds the token the moment we know its value. Bridge supervisors get a token=/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_connection gates POST/PUT/PATCH/DELETE on a matching X-CSRF-Token request header (rejection: 403 csrf token mismatch). Exempt: /api/auth/{bootstrap,register,login,logout,api/auth} — they establish the session that later carries the cookie. New web/static/js/csrf.js monkey-patches window.fetch so every state-changing request automatically echoes the cookie value; loaded as the first script in chat.html, the inline terminal script in _build_html, and lab.html. Test harness (tests/test_web_api.py:_client) gains an httpx event 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 JWT sub at /api/session time. _check_pty_owner(session, cookie) is consulted at /api/stream / /api/input / /api/resize — any other authenticated user trying to reach a known sid gets 403 not session owner. Password-only mode (no JWT) keeps owner_uid=None and 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_DENY refuse host-destroying patterns regardless of permission_moderm -rf / and its --recursive/--force variants, 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 !cmd escape, and all three bridges' !cmd paths. Plus NUL-byte + control-char + 64 KB length rejection on every Bash invocation.

    Filesystem credential denylist. tools/security.py:_check_path_allowed now 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. Set CHEETAHCLAWS_FS_NO_SANDBOX=1 to bypass when intentionally auditing your own secrets. Independent of allowed_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) and CHEETAHCLAWS_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 uses Path.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_env strips 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 any env map an .mcp.json config 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_uid now branches on sys.platform: Linux keeps SO_PEERCRED, macOS / *BSD goes through ctypes-loaded getpeereid(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 with CHEETAHCLAWS_WEB_SECRET). Terminal one-time password from secrets.token_urlsafe(6)[:6] (~30 bits, online-bruteable) to secrets.token_urlsafe(32) (~190 bits). cc_config.save_config strips permission_mode=accept-all before persisting — once-confirmed escape hatches no longer outlive the session that set them. session_store.save_session wrapped in a module-level Lock + explicit BEGIN IMMEDIATE / ROLLBACK so two threads writing the same session_id no longer silently drop one set of changes. agent_runner.py err_msg initialised before the try block (defends against a NameError on first iteration if _handle_permission_request returns "error"); quota.QuotaExceeded matched by isinstance instead of class-name string. compaction.compact_messages wraps stream_auxiliary in try/except + falls back to the original messages instead of crashing the agent loop. providers._recover_args_from_text caps 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_md get TTL caches (30 s / 10 s, keyed by cwd) so the per-turn git rev-parse / status / log and CLAUDE.md re-read stop showing up in profiles. cc_mcp/client.py reader loops use dict.pop() instead of in+index so a late response after a timeout doesn't race the request side. tool_registry._cache_key adds session_id dimension so a Read(/etc/...) cached for one session never leaks to another. session_store.search_sessions LIKE-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:_renderModels previously injected server-supplied model names directly into an onclick="app.selectModel('${full}')" attribute — now uses data-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=0 hard-disables the bridge !cmd shell entirely (default 1, owner-bound by chat_id whitelist anyway). CHEETAHCLAWS_FS_NO_SANDBOX=1 lifts the credential denylist. CHEETAHCLAWS_DISABLE_PLUGINS=1 / CHEETAHCLAWS_PLUGIN_ALLOWLIST=… / CHEETAHCLAWS_MCP_TRUST_ENV=1 control 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_errors init, permission double-answer race, _broadcast iter race, and the QuotaExceeded classname 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.py God-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-9 branch): 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 notify forwarding. The subprocess-runner reader loop's notify IPC branch used to drop the payload on the floor (F-6/7/8 didn't exist yet). Now it routes through cc_daemon.bridge_supervisor.notify(kind, text). The runner can target a specific bridge via msg["bridge"] (e.g. "telegram") or omit it for a "*" broadcast. agent_runner_notify events 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 RestartPolicy dataclass: mode (none | on-crash), max_restarts, backoff_base_s, backoff_cap_s, backoff_jitter_s. Frozen + a pure next_delay(restart_count) so the decision matrix is unit-testable. agent.start accepts the five fields flat (validation rejects cap < base which would clamp every attempt down to a useless ceiling). On a crash the reader's finally arms a threading.Timer(delay, _do_restart, ...); the Timer respawns via a swappable spawner hook (_RESTART_SPAWNER for tests) and carries restart_count forward. 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...

Read more

v3.05.79

Choose a tag to compare

@chauncygu chauncygu released this 10 May 22:27
d7fc51a
  • 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, /status produced zero response in Docker / --web deploys because _start_headless_bridges() only wired run_query and agent_state on the shared session_ctx — never handle_slash. The bridge poll loops gate on if slash_cb: and fell through to continue before the 📩 Telegram: log line, so the failure was invisible in docker compose logs -f. Fix: extracted the slash handler (originally inlined in repl()) 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() called remove_chat_session(sid) without the user_id the function now requires for ownership-check parity — every reaper tick raised TypeError, killing the daemon thread, so stale ChatSession objects accumulated forever in the in-memory cache. Fix: capture (sid, user_id) pairs from the cached ChatSession objects 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 → + New and direct-typing both drop new sessions into that folder, with a Chat · 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 a folders table, chat_sessions.folder_id nullable FK, in-place PRAGMA table_info + ALTER TABLE migration in init_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_stream no longer double-broadcast to WS) and --web --model X persistence. Tests: +16 new across test_web_api.py (folder CRUD, batch ops, reaper regression) and the new test_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 X actually 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_sync was both returning events inline in the HTTP response and broadcasting the same events to the WS subscribers of the same client; chat.js then iterated data.events AND fired _handleEvent from ws.onmessage, rendering every reply twice. Same bug in handle_slash_stream for SSE-streamed long commands (/brainstorm, /worker, /agent, /plan). Both helpers now deliver events through a single channel — HTTP/SSE only — so _handleEvent runs exactly once per event. Background-thread events (sentinel flows, agent runs) are unaffected: by the time the worker thread emits, _broadcast is already restored to the live WS broadcaster in finally. (2) --web --model X was silently ignored. The CLI override branch only ran in the interactive-REPL path; the if args.web: branch loaded config straight from disk and started the server, so python cheetahclaws.py --web --model custom/qwen2.5-72b would happily boot but every request handler reloaded ~/.cheetahclaws/config.json with the previous model name (e.g. gemma-4-31B-it), producing a confusing 404: model does not exist against the new endpoint. Fix: cheetahclaws.py now persists args.model to config before calling start_web_server, matching the documented behavior; provider:modelprovider/model normalization 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) with custom/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_limit now backfills PROVIDERS["custom"]["context_limit"] so compaction sees the live /v1/models value; per-call shrink based on actual prompt size keeps input + output + 1024 safety ≤ ctx. compaction.get_context_limit gains an optional config arg 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 in agent.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 in agent_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 via auto_agent_dup_summary_limit (0 disables). (4) Agent output paths under ~/.cheetahclaws//agent wizard now resolves relative output filenames (e.g. research_notes.md) to absolute paths under ~/.cheetahclaws/agents/<name>/output/ instead of CWD; AgentRunner exposes runner.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.pdf failure 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 call Read instead of SummarizeLargeFile — 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_summarize helper (tools/files.py). When Read or ReadPDF would 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, calls SummarizeLargeFile, 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.). New safe_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_check lambda calls _maybe_redirect_to_summarize after _read returns; 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 updates tools/files.py and tools/__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 via execute_tool("Read", ...) confirming the wrapper applies the redirect ...

Read more

v3.05.78

Choose a tag to compare

@chauncygu chauncygu released this 09 May 01:37
61e618d
  • May 8, 2026 (v3.05.78): May 8, 2026: F-2/F-3 follow-ups + CI unblock (feature/fix-f2). Main has been red since 9c01237d (the trading-agent #99 merge) because tests/test_packaging.py::test_required_module_imports[modular.trading.ml] (issue #97 regression test) caught that modular/trading/ml/features.py and modular/trading/portfolio.py import 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 from features.py; defer numpy to inside stacker.py:train() / predict_proba() past their early-return paths; gate portfolio.py's numpy behind try/except; add pytest.mark.skipif on 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: move monitor.scheduler.start(...) past the listener bind in cc_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 start fires before the daemon writes its discovery file (both schedulers would otherwise race on last_run_at); flip cc_daemon/schema.py to PRAGMA synchronous=NORMAL (safe under WAL, 8× faster EventBus.publish — 305 μs/event → 39 μs/event, important for streaming agent output); clarify in jobs.py / monitor/store.py / docs/architecture.md that the JSON→SQLite migration is one-way (PR #101's wording implied a fallback read path that doesn't exist); update docs/RFC/0002-daemon-foundation-roadmap.md F-2/F-3 status from OPEN → MERGED. Branch: feature/fix-f2.

  • Research lab Phase A — autonomous multi-day research; WeChat smart-reply + /draft semi-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>] reconstructs LabState from 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 until target_score / max_iterations / plateau / budget; /lab backlog add <topic> --iterate --target=N --max=N --prio=N queues many topics, /lab daemon start runs them 24/7 in a single-worker loop with crash-recovery (reset_running_backlog unsticks stale rows on next start); /lab models prints 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 legacy lab_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 your filehelper (文件传输助手); reply with 1/2/3/AA 1 to send, freeform text to customise, x to skip, q for queue. SQLite-persisted at ~/.cheetahclaws/wx_smart_reply.db (in-memory fallback on init failure); contacts JSON at ~/.cheetahclaws/wx_contacts.json is 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> against wx_contacts.json; when invoked from a bridge channel (WeChat / Telegram / Slack), candidates are also echoed back to the originating uid + stashed in bridges.draft_cache so 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.py now uses 429-aware backoff (10/30/60/120s vs 0.5/1/2/4s for 5xx) and honours Retry-After headers (capped at 180s); the lab surveyor stage grounds in real research.aggregator.research() hits before invoking the LLM (top-30 academic+tech results passed as context, persisted as survey_search_hits artifact 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_numbered dedupes by content (questioner emitting 1..5\n1..5 keeps 5, not 10); the citation verifier now has a per-citation 30s concurrent.futures hard 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 start and /lab start now 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; /config parses 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

Choose a tag to compare

@chauncygu chauncygu released this 07 May 21:32
530dbe7
  • May 7, 2026 (v3.05.77): MCP HTTP/SSE transport + OAuth 2.0 PKCE, .env loader, ANTHROPIC_ENDPOINT corporate-proxy override, AskUserQuestion UI polish (#88, #89)cc_mcp/client.py now speaks Streamable HTTP (POST → text/event-stream reply) in addition to stdio and pure SSE, with the Accept: application/json, text/event-stream header servers like sap-jira require to stop 406-ing. OAuth 2.0: new cc_mcp/oauth.py implements 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.json at mode 0600 with the parent directory locked to 0700. 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 advertised scopes_supported (preferring mcp if listed, otherwise the first one, otherwise omitted entirely so servers without an mcp scope no longer reject with invalid_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 list subcommand with full-width tool descriptions wrapped at 72 cols. Server name sanitization: hyphenated names like github-tools now resolve correctly through the mcp__server__tool qualified-name path. .env loader: _load_env() runs at the very top of cheetahclaws.py before any other import reads os.environ, so .env keys are visible to every module without losing existing-shell-var precedence (os.environ.setdefault). MCP HTTP headers values are passed through os.path.expandvars, so "Authorization": "Bearer $GITHUB_TOKEN" works out of the box. ANTHROPIC_ENDPOINT env var (also reachable via .env) overrides the persisted anthropic_endpoint config and is used by both the streaming Anthropic client (providers.py passes base_url=... to anthropic.Anthropic) and the connectivity probes in /doctor / setup wizard, letting corporate proxies swap api.anthropic.com cleanly. UI: AskUserQuestion is auto-approved alongside EnterPlanMode/ExitPlanMode (it's an interactive tool by definition, a permission prompt was redundant), the spinner and result line are suppressed in print_tool_start/end, the question text is rendered through clr() with Markdown stripped (**bold**, `code`, *italic*), and option indices/descriptions are colorized. The REPL prompt now prints a full-width rule via os.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.py previously only had _tg_send (text via sendMessage), 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 inbound document handler that saves uploads to /workspace (or tempfile.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_runner that mails any file written by the Write tool — FIFO-paired with the in-flight file_path, skipped on Error: / 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 real inline_keyboard (callback_data="cc:<prompt_id>:<value>", _handle_callback_query does 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_interactive passes [(✅ Approve, y), (❌ Reject, n), (✅✅ Accept all, a)]. Backward-compatible: every existing ask_input_interactive call site (no options=) 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-all was a red herring; the bridge simply lacked the upload code path.
  • May 2, 2026: Docker chat UI assets 404 follow-up (#73) — web/server.py now resolves _WEB_DIR via importlib.resources.files("web") instead of Path(__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] for web widened to static/**/* so non-editable wheels reliably ship the full web/static/ subtree. Plus a new docs/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 via host.docker.internal, workspace bind-mount for Samba sharing. --web mode 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: AskUserQuestion no longer deadlocks the terminal (#69) — synchronous render+read instead of a queue/event the agent thread can't drain. messages_to_openai emits content: "" instead of null for tool-only assistant turns so Ollama's OpenAI-compat endpoint stops 400-ing with invalid message content type: <nil>; 400 / BadRequestError reclassified as a non-retryable INVALID_REQUEST so a malformed body no longer trips the circuit breaker (#71).
  • Apr 24, 2026: Support Deepseek V4 models, multi-model prompt adaptation — single shared default.md baseline + 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_content round-trip + thinking: ON by default). fix(setup-wizard): tolerate api_key_env=None for ollama/lmstudio (#59)