Releases: deepfounder-ai/castor
Release list
v0.25.0 — v0.25.0 — MiniMax tool-calling, reliable Telegram streaming, Docker server package
v0.25.0 — MiniMax tool-calling, reliable Telegram streaming, Docker server package
Reliability + deploy release. The headline is end-to-end MiniMax-M2 tool use (it now actually runs browser/secret/extended tools instead of leaking XML to the chat), a rewritten Telegram streaming path that no longer goes silent or loses the reasoning block, a knowledge-graph de-duplicator, an Inspector pass, and a production-ready Docker package with a persistent-memory volume. No schema migrations. No breaking changes. Drop-in upgrade.
MiniMax-M2 (and Anthropic-style) tool calls now execute
MiniMax-M2.7 emits tool calls as Anthropic-style XML (<invoke name="…"><parameter name="…">) in the content stream, not as native delta.tool_calls. Castor mishandled this end-to-end — the tags leaked into the chat as raw text and the tools never ran ("castor broke on a browser request"). Fixed across the loop:
- Text-to-tool extraction learned the
<invoke>/<minimax:tool_call>dialect (new Pattern 1b), ordered ahead of the fuzzy prose heuristics so abrowser_opencall with a URL isn't mangled. - Tool-call XML is suppressed from the streamed reply — the markup is executed, not shown. The final message is clean instead of
document.querySelector(…) </minimax:tool_call>+ a bare tools list. - Extended tools auto-activate. MiniMax calls
browser_wait_for,schedule, etc. straight from training without a priortool_search. The main chat agent now recognises a text-emitted call to ANY known tool, executes it, and activates it for later turns. Subagents keep their restricted whitelist as the gate. - The bot is never silent. A turn that ended on a tool call with no closing summary used to drop the whole Telegram message (
if response:); it now sends when either the reply or the streamed buffer has content, with a "done" acknowledgement fallback.
Telegram streaming: thinking that stays put
- No more truncated replies. Inline-thinking models split
</think>answeracross one delta; the answer text riding alongside the closing tag was dropped. The loop now splits on the tag boundary and emits both sides — losslessly, for web streaming too. - Reasoning no longer vanishes mid-task. On a long multi-round turn the ephemeral rich draft could expire (or get rejected when oversized), latching the render to a placeholder path that dropped the thinking block. The placeholder now shows a
💭reasoning block too, a keepalive thread refreshes the live view during long gaps (slow LLM rounds, multi-second browser tools), and both draft and placeholder cap the partial answer so a long turn can't produce an oversized draft.
Knowledge graph: duplicate entity de-dup
Night synthesis spawned a fresh entity node every run instead of updating the existing one (a fuzzy search(limit=1) missed the exact match when a near-name out-ranked it), so the graph filled with up to 14× Drayage / LinkedIn nodes. Now:
synthesis._upsert_entitylooks the node up by exact name, merges every copy into one, and drops the extras — it stops spawning duplicates and self-heals touched entities.- New "merge duplicates" button in the graph toolbar +
POST /api/knowledge/graph/dedupecollapse same-named nodes (relations + observation counts preserved; identity is by name so links stay intact). - The graph endpoint also merges by name at render time, so the view is clean immediately.
Inspector
A pass over the right-side Inspector:
- Context-window gauge now refreshes on a settings save (was stuck showing the pre-save value), shows
1Minstead of1000k, and falls back to the real settings dump instead of a deadstate.settingsreference.model_contextis now settable in Settings → Inference (the gauge tooltip already pointed there). - Recalled memories — removed a dead KB-preview fallback that left the "RECALLED · this session" counter stuck and could imply recall the agent never made; the live WS path is authoritative. The
livebadge no longer shows on an empty turn. - Active tools now includes the
tool_search-activated extended tools for the thread (dashed chips), and the header count matches the deduped chips. - Latency — the decode row is labelled
tok/s(it's a rate).
Docker: production server package with persistent memory
The shipped Docker setup is now actually deployable:
- Dockerfile fixes — the old
CMDran a non-existentqwe-qwecommand (the console script iscastor), and it never copiedprompts/orschemas/, so goals and presets crashed. Added a/dataVOLUME, sane env defaults, and a/api/statusHEALTHCHECK. docker-compose.ymlpulls the prebuilt GHCR image, bind-mounts./castor-data:/dataso all state (SQLite, Qdrant vectors, wiki, skills, uploads, presets, logs) survives restarts and upgrades, reads config from.env, and setsshm_size: 1gbso Chromium doesn't crash.- New
.env.example(provider URL/model/key,CASTOR_PASSWORDweb auth) anddocs/DEPLOY.mdwith quick-start, build-from-source, update, backup, and terminal-access (with the Qdrant disk-lock caveat) instructions.
Internal: legacy cleanup (~1150 lines removed)
- Removed the legacy v1 agent loop and the
agent_loop_v2flag — v2 has been the only path in production. With it went the v1-only self-check cluster and theself_check_enabledsetting. - Wired trajectory recording into the live loop (opt-in via
trajectory_enabled) — it existed but was never attached. - Dropped a batch of dead symbols (
discover_first,completed_count,provider_kind_from_url, server file-text helpers, unusedagent_budgetlimit fields, a deadscheduler._log_runbranch, theSKILLS_DIRalias, and stale agent-event constants/methods).
Full diff: v0.24.0...v0.25.0.
v0.24.0 — v0.24.0 — Telegram Rich Messages (Bot API 10.1) + MiniMax provider
v0.24.0 — Telegram Rich Messages (Bot API 10.1) + MiniMax provider
Feature release. The headline is end-to-end Telegram Rich Messages — Castor now renders the full Bot API 10.1 formatting dialect — plus a new MiniMax provider, a quieter Telegram chat, and test-suite hygiene fixes. No schema migrations. No breaking changes. Drop-in upgrade.
Telegram: full Bot API 10.1 Rich Messages
Telegram's Bot API 10.1 (2026-06-11) added sendRichMessage / editMessageText(rich_message=), taking an InputRichMessage with a markdown or html string that Telegram parses server-side. Castor now ships the agent's reply through that as the PRIMARY send path, so the agent's Markdown renders as actual rich content:
- Headings (
#…######), tables, inline + display math ($x^2$,$$E=mc^2$$), ordered / unordered / task lists (real checkboxes), dividers, block + pull quotations, footnotes, marked text (==x==), sub/superscript. - Spoilers (
||x||), underline, custom emoji, and inline media embeds (→ photo / audio / video / GIF). - Live
<tg-thinking>streaming — private chats now stream the agent's reasoning in an ephemeral "Thinking…" block (viasendRichMessageDraft, which also fixes the long-broken draft path that always failed withRANDOM_ID_INVALID). The final message stays clean; reasoning lives only in the transient preview. - The classic MarkdownV2 / HTML converters remain as the graceful fallback for deployments whose Bot API predates 10.1, with capability detection cached per process.
Along the way: agent-emitted raw HTML now renders instead of showing literal <b> tags; the blockquote MarkdownV2/HTML divergence was fixed (consecutive quote lines group into one block); and a terse Telegram-only capability hint tells the agent the surface supports rich Markdown + inline media so it uses them when helpful (the shared soul stays clean for web / CLI).
Telegram: inbound non-text message types
Inbound parsing covered only text, caption, photo, document, and voice/audio — every other type (location, venue, contact, poll, dice, sticker, video, video_note, animation) hit a silent-drop gate and the user got no reply. _describe_nontext_message now maps each to a short bracketed text injection so the agent actually sees them.
Telegram: system cron tasks no longer DM the owner
The owner was getting a ⏰ __synthesis_continuous__ — No pending items DM every 15 minutes, plus similar noise from synthesis / coach / trajectory-prune. Cron notifications are now gated to user-created routines only; __name__ system tasks stay silent.
New provider: MiniMax
MiniMax (international) drops in as an OpenAI-compatible preset at https://api.minimax.io/v1 (China: https://api.minimaxi.com/v1) — Bearer auth, no GroupId, sold as a token subscription. Default model suggestions for the M2 family (M2.5 / M2.1 / M2 / M1 / Text-01), editable in the UI; key-hint links straight to the MiniMax interface-key page.
Test-suite hygiene
qwe_temp_data_dirfixture leakedcastor_pytest_*tempdirs on locked-Qdrant / crash teardown — one dev tree hit 8157 dirs / 24 GB. Now self-heals: startup sweep of stale dirs, apytest_sessionfinishcleanup of this run's dirs, and Qdrant-close-before-rmtree.- Migration tests moved off
tempfile.mkdtemp()(which leaked) to pytest'stmp_path, and their sqlite connections close viacontextlib.closing.
Dependencies
9 Dependabot bumps merged: rich ≥15, Pillow ≥12.2, pyyaml ≥6.0.3, python-docx ≥1.2, markitdown ≥0.1.6, and four docker GitHub Actions (metadata/setup-buildx/login/build-push).
Upgrading
git pull + restart. No config or schema changes.
To use the live Telegram rich formatting, just chat with the bot — replies render rich automatically. To use MiniMax, pick it in Settings → Provider, paste your token-subscription key, and choose a model.
v0.23.4 — v0.23.4 — Secret-scrub bundle (3 CRITICAL fixes)
v0.23.4 — Secret-scrub bundle (3 CRITICAL fixes)
Security-focused patch release. Closes the secret-scrubbing bypass family flagged by the whole-codebase architecture review (cross-cutting §4.1): three CRITICAL findings and one HIGH, all in a single PR. No schema migrations. No breaking changes. Drop-in upgrade.
What changed
Three persistence paths were skipping the redaction layer that memory.save has used since v0.17.18. Every site now shares the same secret_scrub.scrub_text / scrub_fact engine.
C1 — db.save_message (chat history)
Chat history was the project's largest secret surface: every user turn, every tool call, every tool result landed in messages.{content, tool_calls, meta} verbatim. The same redaction layer that save_checkpoint uses in-flight is now applied at message persistence. The fact_save({"key": "linkedin_password", "value": "..."}) structural special-case is mirrored so plain-string passwords keyed by a self-identifying name are caught — not just provider-regex shapes.
C2 — synthesis.py (entity / wiki summaries)
The night synthesis pass calls memory._save_single directly to persist LLM-summarised entity and wiki blobs. memory.save scrubbed at its entry, so direct callers bypassed redaction. _save_single now scrubs by default; memory.save passes scrub=False (it already scrubbed at the boundary). Synthesis paths pick up the scrub for free.
C3 — trajectory.tool_start / tool_end (JSONL audit trail)
Trajectory recorder is opt-in but ships with a 30-day default retention — a tool that echoed a secret would persist it on disk longer than the chat that triggered it. args dict and result_preview now run through secret_scrub. The fact_save structural special-case is reused so passwords stored under {"key": "...", "value": "..."} shape are caught.
H4 — trajectory.prune_old actually wired
prune_old(days) was defined since v0.22 but never called — the "30-day rotation" was documented but never fired. New __trajectory_prune__ system task at 04:00 daily, registered only when trajectory_enabled, routes through _execute_task to trajectory.prune_old(trajectory_keep_days). Stateless fast path — no LLM, zero cost.
Why this matters
The architecture review's verdict was "the security story is mostly honoured in the spec, but the implementation has at least three places where secret-scrubbing is bypassed on real persistence paths. Close those (small surgical fixes) and Castor's defensive posture matches what its docs already promise." This release closes those three places.
Tests
1590 passing (was 1500 in v0.23.3). 16 new tests in tests/test_scrub_bundle.py pin every surface area:
_save_singlescrubs by default;scrub=Falseopt-out works.memory.save→_save_singlechain scrubs once, no double-warning.save_messagescrubs content / tool_calls (incl. fact_save shape) / meta.save_messagepasses clean text byte-for-byte.tool_startscrubs args dict, incl. fact_save keyed-as-secret value.tool_endscrubs result_preview; empty-result safe._register_trajectory_pruneis opt-in (skips when trajectory disabled)._execute_taskroutes the task name toprune_old._is_routinereturns False (system task stays on fast path).prune_oldactually deletes stale*.jsonlfiles.
The only failure in the full suite is tests/test_serial_port_skill::test_list_ports_empty_includes_platform_hints — pre-existing platform flake on main, unrelated to this PR.
Upgrading
git pull + restart. No config or schema changes.
To audit pre-v0.23.4 chat history for secrets, run the existing memory.reindex_from_markdown recovery flow (added in v0.23.3) — atoms re-embedded from markdown source get re-scrubbed on the way back into Qdrant.
v0.23.3 — v0.23.3 — Coach, recovery helpers, polish
v0.23.3 — Coach, recovery helpers, polish
Patch release on v0.23.2: opt-in daily anti-pattern coach, a recovery path for Qdrant ↔ markdown desync, a sharper --doctor warning for onnxruntime-gpu (community PR), and a brand refresh on the web UI.
No schema migrations, no breaking changes. Drop-in upgrade.
Coach — daily anti-pattern scan (opt-in, no LLM cost)
Inspired by Microsoft's AI Engineer Coach VS Code extension. A small scheduled job (__coach_daily__, fires at 09:00) walks the last N days of agent_runs + goals + scheduled_tasks and writes a markdown summary to memory + an archive copy under $DATA_DIR/uploads/coach-YYYY-MM-DD.md. Pure SQL/Python, zero LLM cost.
Six built-in rules:
mega_session— non-subagent run >30 min (loop/stuck candidate)cost_outlier— any single run ≥ $1.00capitulating_goals— goal status='done' with failed subtasks or no acceptance criteriashell_heavy— input/output token ratio >50:1 across 3+ runs (proxy for shell-poking)synthesis_overspend— system synthesis crons burning more than $0.10/day (regression guard for the v0.23.2_is_routinefix)no_skills_used— 30+ chat sessions with zero skill / tool_search hits
Each finding ships with severity, headline, and an actionable recommendation. Dry-run against the developer's actual ~/.castor surfaced 5 real anti-patterns including the historical synthesis cost leak.
Opt-in via setting:coach_enabled = 1. Window configurable via setting:coach_lookback_days (default 7). 20 unit tests pin the rules + scheduler wire-up.
Knowledge graph recovery: memory.reindex_from_markdown
User-facing symptom this fixes: the knowledge-graph view in the Web UI is empty and memory.search returns 0 results, despite hundreds of memory atoms visible via the markdown layer (~/.castor/memories/atoms/).
Phase-1 Living Memory writes Qdrant + markdown as siblings. If Qdrant gets wiped or rebuilt — corrupt-rebuild, manual /api/knowledge/graph/clear, or a migration that drops the collection — the markdown layer survives but the search indexes are gone. There was no reverse path to recreate them (memory_store.backfill_from_qdrant goes the wrong direction).
New memory.upsert_with_id(point_id, text, tag, ...) and memory.reindex_from_markdown(skip_existing=True):
- Scrolls every markdown atom under
$DATA_DIR/memories/atoms/ - Re-embeds dense + sparse vectors
- Upserts to Qdrant under the SAME point id (entity
relations[]cross-references stay valid) + FTS5 skip_existing=True(default) scrolls Qdrant up-front to collect already-present ids and skips them — a no-op on a healthy install- Never raises; malformed atoms count as
errorsand the sweep continues
New POST /api/knowledge/reindex endpoint exposes it for one-click recovery from the UI / CLI.
Verified on the affected install: 159 scanned, 133 written, 26 skipped, 0 errors. The knowledge-graph endpoint immediately returned 19 nodes + 38 links again.
Web UI: server-broadcast notifications no longer open a phantom bubble
Carry-over fix from the v0.23.2 release-day investigation, restated here because more notification types were caught. handleWsMessage short-circuits all 12 broadcast notification types (cron, compaction, update_*, telegram, knowledge_*, task_update, canvas_*, get_frame/frame_request, interrupted_turn) BEFORE the streaming-gate that creates an assistant bubble. The cron handler additionally filters __-prefixed system jobs so users aren't toasted by their own background curator every 15 minutes. A JS-contract test walks server.py for new _broadcast({"type": ...}) sites — adding a notification type without a client-side handler now fails CI rather than ships as a phantom bubble.
Doctor: onnxruntime-gpu warning is now actionable (closes #8)
Community contribution from @gberaberry-sys (PR #40).
The doctor check that warns about onnxruntime-gpu (3 GB of CUDA DLLs Castor doesn't use under CPU-only embeddings) now:
- Reports the disk space that would be freed (e.g.
~3.1 GB disk). - Softens the warning when
CUDA_PATH/CUDA_HOMEis set — the user installed CUDA Toolkit intentionally, so the message switches to an informational "embeddings use CPU by default; GPU package is unused unlessembed_device=cuda." - Skips the warning entirely when
setting:embed_device = cudais explicitly set — user knows what they're doing.
skill_creator AST repair (closes #14)
Carry-over from v0.23.2 — restated for the changelog. New _fix_stub_branch_outside_code does AST-level repair for the LLM anti-pattern where small models emit elif name == "x": pass and then write the real implementation outside the branch at function-body indent. The line-based _fix_elif_body_indent catches the common shape; the AST pass handles blank lines, comments, chained-elif tail-stubs, and tab/space inconsistencies. 15 new tests pin the contract.
Brand refresh
static/logo.png updated. Apple touch icon and favicon regenerated from the same source. logo-spicy.png (the easter-egg variant toggled by state.spicy) intentionally left alone.
Tests
1500+ passing (was 1453 in v0.23.2). 29 new tests across test_coach.py (20), test_memory_reindex.py (9), plus the cli.py doctor improvements from PR #40.
Upgrading
git pull + restart. No config or schema changes.
If you were affected by the empty-knowledge-graph desync, run once:
curl -X POST http://localhost:7860/api/knowledge/reindexTo enable the coach (off by default):
# Via the Settings UI, or:
import db; db.kv_set("setting:coach_enabled", "1")v0.23.2 — v0.23.2 — Phantom generating bubble fix
v0.23.2 — Phantom "generating" bubble fix
Critical user-facing fix
Phantom "generating" assistant bubble appeared out of nowhere on idle chats and blocked further sends.
User report: idle chat, agent's last reply already delivered, everything looked done — and suddenly a "castor 09:39 PM generating" status appeared with the typing indicator on. The bubble never closed, so the composer stayed in a busy state and new messages couldn't be sent.
Root cause: static/index.html::handleWsMessage short-circuited only on a few notification WS types (task_update, canvas_*, get_frame, interrupted_turn). The server emits 8 more notification types via _broadcast to every connected client regardless of which thread is in view — cron, compaction, update_progress, update_done, telegram, knowledge_progress, knowledge_gpu_warning, knowledge_done. Each slipped past the (incomplete) short-circuit list and hit the streaming-message creation gate, which opened a pending assistant bubble that NEVER received the done event that notifications don't emit.
The exact 09:39 PM scenario: the __synthesis_continuous__ cron fires every 15 minutes, the cron callback broadcasts a cron WS message, every open web client opens a phantom bubble. Same class of bug as task_update (fixed in v0.18.3) but for the remaining notification types that were never wired up.
Fix: explicit short-circuit handler for every broadcast notification type with appropriate UI treatment (toast for transient events, silent for events with their own panel). System-internal cron jobs (__synthesis_continuous__, __heartbeat__) are silently filtered so the user isn't toasted by their own background curator every 15 minutes.
Auditing guard
The original bug pattern can recur whenever someone adds a new _broadcast({"type": "..."}) call in server.py and forgets the corresponding client-side handler. New JS-contract test (tests/test_ws_notification_short_circuit.py) walks server.py for every _broadcast type literal and asserts the client has a short-circuit BEFORE the streaming gate. Adding a new notification type without wiring the client will now fail CI rather than ship as a phantom bubble.
skill_creator: AST-level repair (closes #14)
Issue #14 documented a recurring LLM failure mode in the skill-creation pipeline: small models emit a tool-dispatch elif name == "...": with body pass and write the real implementation OUTSIDE the branch at function-body indent. The line-based regex fixer (_fix_elif_body_indent) caught the common shape but missed edge cases observed in the workspace_meter and camera_diagnostics field sessions — blank lines between Pass and the stray code, comments in between, chained-elif tail-stub patterns, tab/space inconsistencies.
New _fix_stub_branch_outside_code does AST-level repair: parses the LLM output, walks dispatch If nodes whose tail is body=[Pass], pulls following non-dispatch siblings into the branch's body, re-emits via ast.unparse. Defensive: returns the input unchanged if ast.parse can't handle it (lets downstream syntax check report the real error). Wired in two pipeline call sites (the main custom-code assembly and the SyntaxError recovery path).
15 new tests pin the contract against the exact buggy shapes from the field sessions.
Dependency updates
Dependabot PRs #35-39 applied in batch:
openpyxl 3.1 -> 3.1.5 (patch)
python-pptx 1.0 -> 1.0.2 (patch)
qdrant-client 1.11.0 -> 1.18.0 (7 minor — verified memory + rag still work)
readchar 4.0.0 -> 4.2.2 (minor)
requests 2.31.0 -> 2.34.2 (patch)
Tests
1451 passing, 24 skipped (was 1345 in v0.23.1). 19 new tests added across:
test_ws_notification_short_circuit.py(4) — broadcast notification short-circuits + cron filter + auditing guardtest_skill_creator_ast_fix.py(15) — AST-level repair for issue #14
Plus test isolation fix for CI Python 3.12 (test_provider_error_classification was pulling goal_runner at module level, polluting test_skill_import's database state).
Upgrading
git pull + restart. No config changes, no migrations.
v0.23.1 — v0.23.1 — Goal Runtime Hardening
v0.23.1 — Goal Runtime Hardening
v0.23.0 shipped the Goal Runtime as a new architecture. Real production stress-tests on long LinkedIn networking goals (50+ subagent dispatches across 100+ minutes) surfaced a class of bugs that were invisible in unit tests because the affected code paths were dead in v0.23.0 — the budget cap never fired, the workspace was never isolated, secrets never got scrubbed in goal storage. This release wires them all up and adds the production-shaped tests that should have caught them.
Backwards compatible — no schema break beyond one additive migration (015_agent_runs_goal_id.sql). Goals submitted on v0.23.0 keep running; the new behaviours apply from the next claim onward.
Goal-runtime fixes
-
Per-goal workspace at
~/.castor/workspace/goals/<goal_id>/. Each goal now runs in its own dir. The shared workspace is invisible to the orchestrator inside a goal context — no more 60-round shell-mining sweeps over leftover CSVs / screenshots from prior goals. Symmetric writer/validator path rewriting catches the orchestrator's habit of writing~/.castor/workspace/foo.csvand routes it under the goal dir transparently. -
Budget cap (
budget_usd) actually works. Migration 015 addsagent_runs.goal_idand rolls up costs via a LEFT JOIN. Before this commit,goals.cost_usdwas dead storage (never written), so the orchestrator's per-round budget check read 0 forever. The Goals UI Cost column now displays real spend. -
Provider transient errors → paused (not failed). OpenRouter 402 / 429, upstream 5xx, etc. classify as transient. The goal goes to
pausedwith a per-class backoff (402: 300s, 429: 60s, 5xx: 30s) so a topped-up account or expired rate-limit window lets the goal resume from the latest checkpoint — no work lost. -
Pause-with-backoff prevents reclaim thrash. Without this, the worker's 5s poll cycle would re-claim a 402-paused goal and immediately burn another 402, in a tight loop. The backoff repurposes
lease_expires_atas a "don't reclaim before this time" marker (no schema change). -
~-expansion bug in goal_validators._resolve("~/.castor/workspace/foo.txt")was looking up<workspace>/~/.castor/workspace/foo.txt. Every regex/files check on a~-prefixed path falsely failed with "file does not exist", which forced the acceptance gate to mark working goals as failed. -
Skipped subtasks no longer block the gate. A subtask marked
status="skipped"(e.g. orchestrator hit a quota early) had itsdone_conditionevaluated anyway. The gate now correctly bypasses skipped entries. -
Orchestrator anti-capitulation prompt rule. The "Knowing when you have ENOUGH" section in
prompts/orchestrator.mdused to say "20-30 leads is enough for an MVP." That cap applied to vague quantities only — but the orchestrator also obeyed it for user-specified numbers ("100 invites" → delivered 50). The rule is now scoped: explicit numeric targets in the user_input are LAW; scaling them down is labelled as capitulation, not engineering.
Security: secrets no longer leak through goal storage
Forensic inspection of a production goal showed plaintext credentials in three goal-runtime tables (goal_facts, goal_events, goal_checkpoints.messages_blob). The _scrub_secrets() regex set that memory.save() has used since v0.17.18 was never applied to these new v0.22 storage paths.
-
Shared
secret_scrub.pymodule. Patterns moved out ofmemory.py.scrub_textfor free-form text,scrub_fact(key, value)adds a key-name heuristic — keys namedpassword,api_key,access_token,private_key,session_cookie, etc. fully redact their value regardless of shape, catching plain string passwords that don't match any provider regex. -
Four goal storage paths now scrub on insert.
db.fact_save,db.log_goal_event,db.save_checkpoint, anddb.attach_goal_outputall pass values through the appropriate scrub before write.save_checkpointalso walkstool_calls[].function.argumentsso the orchestrator's habit of putting credentials in dispatch prompts gets caught. -
Natural-language credential phrasing. Added a second regex for "Fill in the password field (#password) with: hunter2" style prose — the dispatch-prompt pattern that exposed a LinkedIn password in production. Keeps innocent technical writing intact.
-
Browser subagent now has direct vault access. Added
secret_get/secret_listto the browser subagent's tool whitelist + a newCredential handlingsection inprompts/orchestrator.md. The orchestrator no longer needs to fetch credentials and ferry them across the trust boundary into dispatch prompts — the subagent fetches them locally and the raw value never enters orchestrator messages, events, or checkpoints.
Behaviour changes
-
Worker daemon costs now roll up per goal. Old goals (created before migration 015) keep showing
cost_usd: 0.0since theiragent_runsrows have nogoal_idlink. New goals get accurate per-goal cost tracking immediately. -
A paused goal with
retry_after_secset is invisible toclaim_next_goaluntil the deadline elapses. Existing pause paths (worker shutdown, user pause) don't set the deadline, so they stay immediately reclaimable — same as before. -
mark_goal_paused(reason, retry_after_sec=N)is the new signature. Old callers (the keyword-onlyreason=form) keep working.
Migrations
015_agent_runs_goal_id.sql — adds goal_id TEXT column to agent_runs with a partial index. Backward-compatible (existing rows get NULL, treated as "non-goal run" by the budget rollup).
Tests
96 new tests across 4 new + several updated files. Full suite: 1345 passing, 24 skipped.
test_goal_workspace_isolation.py(13) — per-goal workspace creation, path-rewrite invariants, cross-goal isolation.test_provider_error_classification.py(15) — 402/429/5xx classification, integration withgoal_runner.run, backoff blocking immediate reclaim.test_secret_scrub_goals.py(25) — every goal storage path scrubs, includingattach_goal_outputand tool_calls.arguments.test_agent_runs.py(+5) —goal_idcolumn persistence,get_goal_total_cost,get_goal/list_goalscost rollup contract.
Upgrading
git pull + restart the server (or castor-worker). The first goal_runner claim on the new code applies migration 015 automatically. No config changes required.
If you have goals paused or failed on v0.23.0 with APIStatusError 402 (OpenRouter out of credits) in the error field, you can manually convert them to paused to make them resumable:
UPDATE goals
SET status='paused', error=NULL, finished_at=NULL,
worker_id=NULL, lease_expires_at=NULL
WHERE status='failed' AND error LIKE '%402%';v0.23.0 — v0.23.0 — Goal Runtime, Native Anthropic, Plugin Framework
v0.23.0 — Goal Runtime, Native Anthropic, Plugin Framework
The biggest release since v0.18.7. Goals turn castor from a chat assistant into an autonomous agent that can work for hours on multi-step tasks — surviving disconnects, process restarts, and context-window pressure.
Long-running multi-step tasks are now first-class citizens. Create a goal in the Goals view and Castor breaks it into a plan, dispatches subagents per subtask, and tracks progress live.
Goals — long-running autonomous tasks
Create a goal ("Research construction costs in Argentina and write a report"), walk away, come back to a completed deliverable. The system plans, delegates to specialized subagents, validates results, and retries on failure — all without user input.
Architecture: Goal -> Plan -> Subagent dispatch. A separate castor-worker daemon claims goals from a durable SQLite queue. Full design doc in docs/superpowers/plans/.
- Worker daemon (
python -m worker) — claims goals via lease, heartbeats, survives crashes. Also runs inline (--oncefor tests, auto-start in web mode). - Orchestrator — breaks the goal into subtasks, dispatches subagents, tracks progress via structured facts.
- 4 subagent types —
research,browser,code,scraper— each with a restricted tool whitelist (the security boundary). Fresh LLM context per subtask, 20-round cap. - Acceptance gate — after the orchestrator returns, validators check each subtask's
done_condition(5 kinds:files_exist,min_count,regex_in_file,shell_returns_zero,http_200). Failures inject a remediation note and re-enter the orchestrator (up to 3 attempts). - Structured deliverables — files, links, reports attached via
goal_attach_output. UI renders Download/Open/Save buttons. - Per-goal browser sessions — parallel goals get isolated browser contexts.
- Budget enforcement — wall-clock seconds + USD caps, enforced at the runner level.
- Live UI — Goals view with plan progress, events timeline, facts tab. Polling at 2s while running, 10s when idle.
New migrations: 011_goals_subtasks_checkpoints.sql through 014_goal_done_conditions.sql.
- Full native client for Claude models (
providers.py) — no OpenAI shim - Three workstreams merged: converters, stream reassembler, routing + 88 tests
- Model routing: local providers (LM Studio / Ollama) via OpenAI-compat, cloud Claude via native SDK
NEEDS KEYbadge + key modal in provider picker
Native Anthropic provider
Direct Anthropic API support without the OpenAI compatibility shim. Three workstreams merged:
- Converters — bidirectional message/tool format translation between OpenAI and Anthropic schemas.
- Stream reassembler — handles Anthropic's SSE delta format (content_block_delta, tool_use blocks) and reassembles into the internal streaming shape.
- Client + routing —
providers.pyauto-routes to the native adapter when the active provider isanthropic.
88 new tests across the three workstreams.
Skills
Plugin framework (Hermes-inspired)
Extensible slot-based plugin system for hooking into agent lifecycle events. Plugins can observe/modify behavior at defined extension points without touching core code.
- 3-layer DB corruption protection — rolling backups, startup integrity check (
PRAGMA integrity_check), graceful shutdown WAL checkpoint - Auto-migration from
~/.qwe-qwe/— users upgrading from the old project name get all data (DB, Qdrant collections, uploads, skills) migrated automatically on first boot - SSL: certifi CA bundle used for all outbound urllib requests
- fastembed warnings suppressed (loguru "Local file sizes do not match" spam gone)
- Browser: per-goal sessions for parallel isolation; auto-recovery on dead sessions;
execute()runs in thread executor to avoid asyncio conflicts
Synthesis trickle mode
Background knowledge curator runs continuously (not just overnight), extracting entities and wiki summaries from recent conversations. Keeps the knowledge graph fresh without waiting for the nightly synthesis run.
Centralized command registry
Slash commands (/goal, /resume, /status, etc.) now registered via a central registry instead of ad-hoc string matching. Easier to add new commands, consistent help output.
Skill export
Companion to skill import (v0.18.7) — export castor skills to the agentskills.io SKILL.md format for sharing via skills.sh or GitHub.
JSONL trajectory recording
Every agent run optionally records a full JSONL trajectory (messages, tool calls, results, timing) for offline analysis, evals, and debugging.
Persistent tool_search activations
tool_search activations now persist per-thread across page reloads. Previously, extended tools unlocked via tool_search("browser") would disappear on refresh.
DB corruption protection
3-layer defense: rolling backups on startup, SHA-256 integrity check, graceful WAL checkpoint on shutdown. Recovers automatically from the most recent valid backup if corruption is detected.
Notable fixes
- Orchestrator browser tool leak — built-in browser skill tools (24) leaked into the orchestrator's tool set, causing the LLM to bypass
dispatch_subagentand burn 80+ rounds driving a browser directly. Fixed via_ORCHESTRATOR_EXCLUDED_TOOLSblacklist. - Goal plan validation — error message listed wrong
done_conditionkinds; fuzzy matching now suggests corrections (files_exists->files_exist); empty plans no longer pass the acceptance gate vacuously. - UI scroll jumps — clicking nav links with
href="#"scrolled to top;render()only preserved scroll for chat view. Now all.scroll-colcontainers retain position across re-renders. - Failed goals UI — failed goals wouldn't open in detail view (
!gR.value.errorguard rejected them). Fixed to checkgR.value.id. - Streaming tool results — reply event was wiping tool results accumulated during streaming.
allStringsguard preserves them. - Soul trait [object Object] — built-in trait descriptions passed raw objects to
esc(). - Tool-call collapse — chat UI collapses tool-call rows beyond N per category to reduce visual noise.
- 16 audit hardening fixes — security, robustness, and observability improvements.
- Auto-migrate from ~/.qwe-qwe/ — seamless data migration on project rename to Castor.
By the numbers
- 1354 tests passing (was ~725 at v0.22.1), 24 skipped
- 14 SQLite migrations (was 10)
- Coverage floor unchanged at 24%
- ~60 commits since v0.22.1
Upgrade
git pull
pip install -e . --upgrade
python cli.py --web --ssl --port 7861Four new migrations apply automatically on first boot. No config changes required. Telemetry consent unchanged.
v0.22.1 — v0.22.1 — Migration reliability fix
v0.22.1 — Migration reliability fix
- fix(db): SQLite migration runner now executes statements one-by-one instead of via
executescript(). This makesALTER TABLE ADD COLUMNmigrations idempotent: ifscheduler._ensure_table()or any other helper pre-creates a column before a migration runs, the "duplicate column name" error is silently skipped rather than aborting the entire migration. Eliminates a test-ordering flakiness introduced after the v0.20 / v0.21 merge. - Internal:
_iter_sql_statements()strips--line comments before splitting on;, so in-comment semicolons (e.g.-- doesn't rewrite rows; each …) no longer produce spurious SQL fragments. - No schema changes, no API changes, no migration files modified.
v0.22.0 — Auto-resume after interrupt
- Every abort (WS disconnect, Stop button, server crash) is now recoverable.
- Web UI shows a banner on reconnect: "Previous turn was interrupted — Resume / Dismiss". The agent picks up from where it left off, not from scratch.
- Telegram exposes
/resumefor the same flow in chat. - Routines auto-resume if the abort was within 5 minutes (configurable).
- CLI Ctrl+C remains an intentional stop — no resume.
- New per-source TTL settings in Settings → Cost → Auto-resume: Web (7 days), Telegram (24h), Routines (5 min).
- Migration 009 adds
resumed_from_run_id+dismissed_attoagent_runs. - Analytics chain resume runs back to their originals.
v0.21.0 — Per-routine budget caps
- Set a USD spending cap per routine, rolling over a configurable window.
- When the cap is reached, the next scheduled fire is SKIPPED with
status='skipped',error='budget_exceeded'in agent_runs — history
shows what happened. The routine resumes once spend drops below the cap. - UI: Routines page shows a budget chip per routine (green / orange /
red based on % of cap). Click to set/clear/edit cap + period. - API:
GET /api/routines/{id}/budgetandPOST /api/routines/{id}/budget. - Migration 010 adds
budget_usd_cap+budget_period_secto
scheduled_tasks. Pre-existing routines have no cap (default).
v0.19.0 — Cost tracking & per-session analytics
- New
agent_runstable replacesroutine_runs: one row per LLM call site
(main loop, synthesis, skill creator, routine fire) with full token + cost
capture. - Online pricing from the LiteLLM community JSON, cached locally, with a
bundled top-10 fallback for offline / air-gapped operation. - Sessions list now shows Tokens + Cost per thread; click a row for a
per-run drilldown with model, source, status, duration, tokens, and cost. - Routines page shows Cost (30d) so you can spot expensive scheduled jobs.
- New Settings → Cost tracking section: pricing URL, auto-update toggle,
manual refresh button. - API:
GET /api/threadsextended withinput_tokens / output_tokens / cost_usd / run_count; newGET /api/threads/{id}/runs,
GET /api/analytics/period,GET /api/pricing/status,
POST /api/pricing/refresh. - Migration 008 atomically replaces legacy
routine_runswith the new
agent_runstable.
v0.18.7 — Canvas (sandboxed HTML side panel) + Skill import (skills.sh / Anthropic SKILL.md spec)
Two big features land together because they're the same idea from opposite directions: richer output → user (Canvas), and more capabilities ← community (Skill import). Plus a Tools & skills tab rebuild so the growing skill list stays usable.
🎨 Canvas — sandboxed HTML in a side panel
The agent can now ship arbitrary HTML to a 480px right-side panel. Three concrete things this unlocks:
1. Interactive forms — the agent asks back, structured
You: Сделай форму записи нового клиента: ФИО, телефон, источник.
Agent: [canvas_prompt html="<form>…</form>" title="New client"]
→ panel slides in on the right
You: *fills the form, hits Submit*
Agent: → receives {name:"...", phone:"...", source:"..."} as the tool result
[memory_save "Новый клиент: ..."]
Saved. Записал.
canvas_prompt blocks until the user submits, exactly like camera_capture blocks until a frame is grabbed. The agent gets the form data back as JSON in the same turn — no manual "type each field into chat" step.
2. Dashboards & status views — pin them, come back next week
You: Покажи дашборд по продажам за последнюю неделю.
Agent: [canvas_render html="<div style='…'>…<canvas id='chart'></canvas>…"]
→ renders a styled HTML page with a Chart.js bar chart
You: Сохрани его как weekly-sales.
Agent: [canvas_save slug="weekly-sales"]
✓
Saved artifacts show up in a new Canvases left-nav view (card grid alongside Memory / Scheduler / Presets). Click a card → panel reopens with the saved dashboard. Reload the chat → the message that opened it has a chip "📊 Canvas: weekly-sales" you can click to reopen.
3. Mockups & prototypes — visual iteration in chat
You: Накидай мокап лендинга для приложения «Поход в горы».
Agent: [canvas_render html="<header>…</header><section class='hero'>…"]
→ panel renders the layout
You: Сделай hero на тёмном фоне и кнопку CTA крупнее.
Agent: [canvas_render …]
✓
The agent iterates the HTML in chat, you see each version side-by-side with the conversation.
Security model — iframe sandbox is load-bearing
<iframe sandbox="allow-scripts allow-forms" srcdoc="...">. Note what's NOT there:
- ❌
allow-same-origin— iframe origin is"null", no parent cookies / localStorage / DOM - ❌
allow-top-navigation— can't redirect the host page - ❌
allow-popups— nowindow.open
The parent listens for postMessage from the iframe and filters by event.source === iframe.contentWindow (origin-string filtering is useless when the origin is "null"). The iframe CAN load public CDN scripts (Chart.js, D3) without cookies, documented as a privacy note in docs/CANVAS.md.
256 KB HTML cap enforced at both skill-side and the REST POST /api/canvas/artifacts endpoint. Charts with inlined SVG fit comfortably; LLMs can't reliably emit more anyway.
Five tools, auto-active
tool_search("dashboard") / "form" / "mockup" / "chart" / "widget" → activates the canvas tools without manual setup:
canvas_render(html, title?, slug?)— fire-and-forget, opens the panelcanvas_prompt(html, title?, timeout_s=300)— blocks until submit / close / timeout, returns user data as JSONcanvas_save(slug, title?, html?)— persist as artifactcanvas_load(slug)— reopen a saved artifactcanvas_list(limit=20)— markdown table of saved artifacts
Full postMessage protocol, sandbox limits, and a reference HTML template live in docs/CANVAS.md.
📦 Skill import — install community skills from skills.sh / GitHub
Anthropic's agentskills.io SKILL.md spec — the same format Claude Code / Claude.ai use — now works in qwe-qwe via a thin adapter layer. Browse skills.sh or any compatible GitHub repo, paste the URL into Settings → Tools & skills → Import skill, click Import.
Recognised URL shapes
https://skills.sh/<owner>/<repo>/<skill-name>https://github.com/<owner>/<repo>/tree/<ref>/<path-to-skill>https://raw.githubusercontent.com/<owner>/<repo>/<ref>/<path-to-skill>/SKILL.md
How the bridge works
skills.sh skills are markdown instructions for an LLM + optional executable scripts. qwe-qwe skills are single Python modules with TOOLS + execute(). The importer generates a thin adapter .py at ~/.qwe-qwe/skills/<name>.py that exposes one tool — <name>_help — returning the full SKILL.md body. Scripts / references / assets land at ~/.qwe-qwe/skills_imported/<name>/. The agent reads them via the regular read_file / shell tools.
Best for knowledge-heavy procedures (PDF manipulation patterns, document conversion recipes, etc.). Pure-code wrappers around a specific API are still better written natively via create_skill.
Safety surface — none of this is optional
| Layer | What it does |
|---|---|
| Domain allowlist | Only skills.sh / github.com / raw.githubusercontent.com / api.github.com. Everything else → HTTP 403 host_not_allowed. |
| SSRF guard | Private / loopback / link-local IPs blocked via socket.getaddrinfo + ipaddress.ip_address. Plus a custom HTTPRedirectHandler re-validates every redirect hop — a public-host fetch can't 302 into 127.0.0.1 or cloud metadata IPs. |
| Name validation | ^[a-z0-9]+(-[a-z0-9]+)*$, ≤64 chars (the agentskills.io regex). |
| Built-in collision | browser, canvas, skill_creator, etc. cannot be replaced even with overwrite: true. Typosquatting defense. |
| License surfacing | Word-anchored SPDX-ish regex + denylist of non-OSS riders (Commons Clause / BUSL / SSPL / Elastic / "Complete terms in LICENSE.txt"). Non-OSS licenses return HTTP 451 license_confirm_required — the UI shows a confirmation panel with the license text before installing. |
| Size caps | SKILL.md ≤100 KB, total fetch ≤1 MB, ≤50 files, binaries / images filtered out. |
| Atomic write | Adapter writes to a tempfile, runs skills.validate_skill on it, then os.replace into final position. A broken renderer can never leave a half-written .py in ~/.qwe-qwe/skills/. |
| Sentinel-protected delete | delete_import checks for the auto-generated sentinel before unlinking. If you replaced an imported skill's .py with hand-written code, your file survives. |
| Audit trail | Every install recorded in the skill_imports table — source URL, SHA-256 hash, license, timestamp. Query via GET /api/skills/imports. |
REST round-trip
curl -X POST http://localhost:7861/api/skills/import \
-H 'Content-Type: application/json' \
-d '{"url": "https://skills.sh/anthropics/skills/pdf"}'Returns HTTP 451 if the upstream license isn't OSS; re-POST ...
v0.22.0 — v0.22.0 — Auto-resume after interrupt
v0.22.0 — Auto-resume after interrupt
- Every abort (WS disconnect, Stop button, server crash) is now recoverable.
- Web UI shows a banner on reconnect: "Previous turn was interrupted — Resume / Dismiss". The agent picks up from where it left off, not from scratch.
- Telegram exposes
/resumefor the same flow in chat. - Routines auto-resume if the abort was within 5 minutes (configurable).
- CLI Ctrl+C remains an intentional stop — no resume.
- New per-source TTL settings in Settings → Cost → Auto-resume: Web (7 days), Telegram (24h), Routines (5 min).
- Migration 009 adds
resumed_from_run_id+dismissed_attoagent_runs. - Analytics chain resume runs back to their originals.
v0.21.0 — Per-routine budget caps
- Set a USD spending cap per routine, rolling over a configurable window.
- When the cap is reached, the next scheduled fire is SKIPPED with
status='skipped',error='budget_exceeded'in agent_runs — history
shows what happened. The routine resumes once spend drops below the cap. - UI: Routines page shows a budget chip per routine (green / orange /
red based on % of cap). Click to set/clear/edit cap + period. - API:
GET /api/routines/{id}/budgetandPOST /api/routines/{id}/budget. - Migration 010 adds
budget_usd_cap+budget_period_secto
scheduled_tasks. Pre-existing routines have no cap (default).
v0.19.0 — Cost tracking & per-session analytics
- New
agent_runstable replacesroutine_runs: one row per LLM call site
(main loop, synthesis, skill creator, routine fire) with full token + cost
capture. - Online pricing from the LiteLLM community JSON, cached locally, with a
bundled top-10 fallback for offline / air-gapped operation. - Sessions list now shows Tokens + Cost per thread; click a row for a
per-run drilldown with model, source, status, duration, tokens, and cost. - Routines page shows Cost (30d) so you can spot expensive scheduled jobs.
- New Settings → Cost tracking section: pricing URL, auto-update toggle,
manual refresh button. - API:
GET /api/threadsextended withinput_tokens / output_tokens / cost_usd / run_count; newGET /api/threads/{id}/runs,
GET /api/analytics/period,GET /api/pricing/status,
POST /api/pricing/refresh. - Migration 008 atomically replaces legacy
routine_runswith the new
agent_runstable.
v0.18.7 — Canvas (sandboxed HTML side panel) + Skill import (skills.sh / Anthropic SKILL.md spec)
Two big features land together because they're the same idea from opposite directions: richer output → user (Canvas), and more capabilities ← community (Skill import). Plus a Tools & skills tab rebuild so the growing skill list stays usable.
🎨 Canvas — sandboxed HTML in a side panel
The agent can now ship arbitrary HTML to a 480px right-side panel. Three concrete things this unlocks:
1. Interactive forms — the agent asks back, structured
You: Сделай форму записи нового клиента: ФИО, телефон, источник.
Agent: [canvas_prompt html="<form>…</form>" title="New client"]
→ panel slides in on the right
You: *fills the form, hits Submit*
Agent: → receives {name:"...", phone:"...", source:"..."} as the tool result
[memory_save "Новый клиент: ..."]
Saved. Записал.
canvas_prompt blocks until the user submits, exactly like camera_capture blocks until a frame is grabbed. The agent gets the form data back as JSON in the same turn — no manual "type each field into chat" step.
2. Dashboards & status views — pin them, come back next week
You: Покажи дашборд по продажам за последнюю неделю.
Agent: [canvas_render html="<div style='…'>…<canvas id='chart'></canvas>…"]
→ renders a styled HTML page with a Chart.js bar chart
You: Сохрани его как weekly-sales.
Agent: [canvas_save slug="weekly-sales"]
✓
Saved artifacts show up in a new Canvases left-nav view (card grid alongside Memory / Scheduler / Presets). Click a card → panel reopens with the saved dashboard. Reload the chat → the message that opened it has a chip "📊 Canvas: weekly-sales" you can click to reopen.
3. Mockups & prototypes — visual iteration in chat
You: Накидай мокап лендинга для приложения «Поход в горы».
Agent: [canvas_render html="<header>…</header><section class='hero'>…"]
→ panel renders the layout
You: Сделай hero на тёмном фоне и кнопку CTA крупнее.
Agent: [canvas_render …]
✓
The agent iterates the HTML in chat, you see each version side-by-side with the conversation.
Security model — iframe sandbox is load-bearing
<iframe sandbox="allow-scripts allow-forms" srcdoc="...">. Note what's NOT there:
- ❌
allow-same-origin— iframe origin is"null", no parent cookies / localStorage / DOM - ❌
allow-top-navigation— can't redirect the host page - ❌
allow-popups— nowindow.open
The parent listens for postMessage from the iframe and filters by event.source === iframe.contentWindow (origin-string filtering is useless when the origin is "null"). The iframe CAN load public CDN scripts (Chart.js, D3) without cookies, documented as a privacy note in docs/CANVAS.md.
256 KB HTML cap enforced at both skill-side and the REST POST /api/canvas/artifacts endpoint. Charts with inlined SVG fit comfortably; LLMs can't reliably emit more anyway.
Five tools, auto-active
tool_search("dashboard") / "form" / "mockup" / "chart" / "widget" → activates the canvas tools without manual setup:
canvas_render(html, title?, slug?)— fire-and-forget, opens the panelcanvas_prompt(html, title?, timeout_s=300)— blocks until submit / close / timeout, returns user data as JSONcanvas_save(slug, title?, html?)— persist as artifactcanvas_load(slug)— reopen a saved artifactcanvas_list(limit=20)— markdown table of saved artifacts
Full postMessage protocol, sandbox limits, and a reference HTML template live in docs/CANVAS.md.
📦 Skill import — install community skills from skills.sh / GitHub
Anthropic's agentskills.io SKILL.md spec — the same format Claude Code / Claude.ai use — now works in qwe-qwe via a thin adapter layer. Browse skills.sh or any compatible GitHub repo, paste the URL into Settings → Tools & skills → Import skill, click Import.
Recognised URL shapes
https://skills.sh/<owner>/<repo>/<skill-name>https://github.com/<owner>/<repo>/tree/<ref>/<path-to-skill>https://raw.githubusercontent.com/<owner>/<repo>/<ref>/<path-to-skill>/SKILL.md
How the bridge works
skills.sh skills are markdown instructions for an LLM + optional executable scripts. qwe-qwe skills are single Python modules with TOOLS + execute(). The importer generates a thin adapter .py at ~/.qwe-qwe/skills/<name>.py that exposes one tool — <name>_help — returning the full SKILL.md body. Scripts / references / assets land at ~/.qwe-qwe/skills_imported/<name>/. The agent reads them via the regular read_file / shell tools.
Best for knowledge-heavy procedures (PDF manipulation patterns, document conversion recipes, etc.). Pure-code wrappers around a specific API are still better written natively via create_skill.
Safety surface — none of this is optional
| Layer | What it does |
|---|---|
| Domain allowlist | Only skills.sh / github.com / raw.githubusercontent.com / api.github.com. Everything else → HTTP 403 host_not_allowed. |
| SSRF guard | Private / loopback / link-local IPs blocked via socket.getaddrinfo + ipaddress.ip_address. Plus a custom HTTPRedirectHandler re-validates every redirect hop — a public-host fetch can't 302 into 127.0.0.1 or cloud metadata IPs. |
| Name validation | ^[a-z0-9]+(-[a-z0-9]+)*$, ≤64 chars (the agentskills.io regex). |
| Built-in collision | browser, canvas, skill_creator, etc. cannot be replaced even with overwrite: true. Typosquatting defense. |
| License surfacing | Word-anchored SPDX-ish regex + denylist of non-OSS riders (Commons Clause / BUSL / SSPL / Elastic / "Complete terms in LICENSE.txt"). Non-OSS licenses return HTTP 451 license_confirm_required — the UI shows a confirmation panel with the license text before installing. |
| Size caps | SKILL.md ≤100 KB, total fetch ≤1 MB, ≤50 files, binaries / images filtered out. |
| Atomic write | Adapter writes to a tempfile, runs skills.validate_skill on it, then os.replace into final position. A broken renderer can never leave a half-written .py in ~/.qwe-qwe/skills/. |
| Sentinel-protected delete | delete_import checks for the auto-generated sentinel before unlinking. If you replaced an imported skill's .py with hand-written code, your file survives. |
| Audit trail | Every install recorded in the skill_imports table — source URL, SHA-256 hash, license, timestamp. Query via GET /api/skills/imports. |
REST round-trip
curl -X POST http://localhost:7861/api/skills/import \
-H 'Content-Type: application/json' \
-d '{"url": "https://skills.sh/anthropics/skills/pdf"}'Returns HTTP 451 if the upstream license isn't OSS; re-POST with "accept_license": true to confirm.
Full pattern doc + reference implementations: docs/SKILLS_IMPORT.md.
🔍 Tools & skills tab — search + collapsible categories
The Tools tab in Settings used to be a flat list. As the skill ecosystem grows (built-ins + user-created + imported), that flat list becomes unscannable. New layout:
- Search box at the top — filters across tool name, description, and category
- Collapsible category headers — Memory / Files / Web / Browser / Hardware / Skills / Meta. Expand only what you need.
- Import skill button in the header — paste URL, install in one step.
The user-created and imported skills appear in their own categories so you can tell where each tool came from at a glance.
...
v0.21.0 — v0.21.0 — Per-routine budget caps
v0.21.0 — Per-routine budget caps
- Set a USD spending cap per routine, rolling over a configurable window.
- When the cap is reached, the next scheduled fire is SKIPPED with
status='skipped',error='budget_exceeded'in agent_runs — history
shows what happened. The routine resumes once spend drops below the cap. - UI: Routines page shows a budget chip per routine (green / orange /
red based on % of cap). Click to set/clear/edit cap + period. - API:
GET /api/routines/{id}/budgetandPOST /api/routines/{id}/budget. - Migration 010 adds
budget_usd_cap+budget_period_secto
scheduled_tasks. Pre-existing routines have no cap (default).
v0.19.0 — Cost tracking & per-session analytics
- New
agent_runstable replacesroutine_runs: one row per LLM call site
(main loop, synthesis, skill creator, routine fire) with full token + cost
capture. - Online pricing from the LiteLLM community JSON, cached locally, with a
bundled top-10 fallback for offline / air-gapped operation. - Sessions list now shows Tokens + Cost per thread; click a row for a
per-run drilldown with model, source, status, duration, tokens, and cost. - Routines page shows Cost (30d) so you can spot expensive scheduled jobs.
- New Settings → Cost tracking section: pricing URL, auto-update toggle,
manual refresh button. - API:
GET /api/threadsextended withinput_tokens / output_tokens / cost_usd / run_count; newGET /api/threads/{id}/runs,
GET /api/analytics/period,GET /api/pricing/status,
POST /api/pricing/refresh. - Migration 008 atomically replaces legacy
routine_runswith the new
agent_runstable.
v0.18.7 — Canvas (sandboxed HTML side panel) + Skill import (skills.sh / Anthropic SKILL.md spec)
Two big features land together because they're the same idea from opposite directions: richer output → user (Canvas), and more capabilities ← community (Skill import). Plus a Tools & skills tab rebuild so the growing skill list stays usable.
🎨 Canvas — sandboxed HTML in a side panel
The agent can now ship arbitrary HTML to a 480px right-side panel. Three concrete things this unlocks:
1. Interactive forms — the agent asks back, structured
You: Сделай форму записи нового клиента: ФИО, телефон, источник.
Agent: [canvas_prompt html="<form>…</form>" title="New client"]
→ panel slides in on the right
You: *fills the form, hits Submit*
Agent: → receives {name:"...", phone:"...", source:"..."} as the tool result
[memory_save "Новый клиент: ..."]
Saved. Записал.
canvas_prompt blocks until the user submits, exactly like camera_capture blocks until a frame is grabbed. The agent gets the form data back as JSON in the same turn — no manual "type each field into chat" step.
2. Dashboards & status views — pin them, come back next week
You: Покажи дашборд по продажам за последнюю неделю.
Agent: [canvas_render html="<div style='…'>…<canvas id='chart'></canvas>…"]
→ renders a styled HTML page with a Chart.js bar chart
You: Сохрани его как weekly-sales.
Agent: [canvas_save slug="weekly-sales"]
✓
Saved artifacts show up in a new Canvases left-nav view (card grid alongside Memory / Scheduler / Presets). Click a card → panel reopens with the saved dashboard. Reload the chat → the message that opened it has a chip "📊 Canvas: weekly-sales" you can click to reopen.
3. Mockups & prototypes — visual iteration in chat
You: Накидай мокап лендинга для приложения «Поход в горы».
Agent: [canvas_render html="<header>…</header><section class='hero'>…"]
→ panel renders the layout
You: Сделай hero на тёмном фоне и кнопку CTA крупнее.
Agent: [canvas_render …]
✓
The agent iterates the HTML in chat, you see each version side-by-side with the conversation.
Security model — iframe sandbox is load-bearing
<iframe sandbox="allow-scripts allow-forms" srcdoc="...">. Note what's NOT there:
- ❌
allow-same-origin— iframe origin is"null", no parent cookies / localStorage / DOM - ❌
allow-top-navigation— can't redirect the host page - ❌
allow-popups— nowindow.open
The parent listens for postMessage from the iframe and filters by event.source === iframe.contentWindow (origin-string filtering is useless when the origin is "null"). The iframe CAN load public CDN scripts (Chart.js, D3) without cookies, documented as a privacy note in docs/CANVAS.md.
256 KB HTML cap enforced at both skill-side and the REST POST /api/canvas/artifacts endpoint. Charts with inlined SVG fit comfortably; LLMs can't reliably emit more anyway.
Five tools, auto-active
tool_search("dashboard") / "form" / "mockup" / "chart" / "widget" → activates the canvas tools without manual setup:
canvas_render(html, title?, slug?)— fire-and-forget, opens the panelcanvas_prompt(html, title?, timeout_s=300)— blocks until submit / close / timeout, returns user data as JSONcanvas_save(slug, title?, html?)— persist as artifactcanvas_load(slug)— reopen a saved artifactcanvas_list(limit=20)— markdown table of saved artifacts
Full postMessage protocol, sandbox limits, and a reference HTML template live in docs/CANVAS.md.
📦 Skill import — install community skills from skills.sh / GitHub
Anthropic's agentskills.io SKILL.md spec — the same format Claude Code / Claude.ai use — now works in qwe-qwe via a thin adapter layer. Browse skills.sh or any compatible GitHub repo, paste the URL into Settings → Tools & skills → Import skill, click Import.
Recognised URL shapes
https://skills.sh/<owner>/<repo>/<skill-name>https://github.com/<owner>/<repo>/tree/<ref>/<path-to-skill>https://raw.githubusercontent.com/<owner>/<repo>/<ref>/<path-to-skill>/SKILL.md
How the bridge works
skills.sh skills are markdown instructions for an LLM + optional executable scripts. qwe-qwe skills are single Python modules with TOOLS + execute(). The importer generates a thin adapter .py at ~/.qwe-qwe/skills/<name>.py that exposes one tool — <name>_help — returning the full SKILL.md body. Scripts / references / assets land at ~/.qwe-qwe/skills_imported/<name>/. The agent reads them via the regular read_file / shell tools.
Best for knowledge-heavy procedures (PDF manipulation patterns, document conversion recipes, etc.). Pure-code wrappers around a specific API are still better written natively via create_skill.
Safety surface — none of this is optional
| Layer | What it does |
|---|---|
| Domain allowlist | Only skills.sh / github.com / raw.githubusercontent.com / api.github.com. Everything else → HTTP 403 host_not_allowed. |
| SSRF guard | Private / loopback / link-local IPs blocked via socket.getaddrinfo + ipaddress.ip_address. Plus a custom HTTPRedirectHandler re-validates every redirect hop — a public-host fetch can't 302 into 127.0.0.1 or cloud metadata IPs. |
| Name validation | ^[a-z0-9]+(-[a-z0-9]+)*$, ≤64 chars (the agentskills.io regex). |
| Built-in collision | browser, canvas, skill_creator, etc. cannot be replaced even with overwrite: true. Typosquatting defense. |
| License surfacing | Word-anchored SPDX-ish regex + denylist of non-OSS riders (Commons Clause / BUSL / SSPL / Elastic / "Complete terms in LICENSE.txt"). Non-OSS licenses return HTTP 451 license_confirm_required — the UI shows a confirmation panel with the license text before installing. |
| Size caps | SKILL.md ≤100 KB, total fetch ≤1 MB, ≤50 files, binaries / images filtered out. |
| Atomic write | Adapter writes to a tempfile, runs skills.validate_skill on it, then os.replace into final position. A broken renderer can never leave a half-written .py in ~/.qwe-qwe/skills/. |
| Sentinel-protected delete | delete_import checks for the auto-generated sentinel before unlinking. If you replaced an imported skill's .py with hand-written code, your file survives. |
| Audit trail | Every install recorded in the skill_imports table — source URL, SHA-256 hash, license, timestamp. Query via GET /api/skills/imports. |
REST round-trip
curl -X POST http://localhost:7861/api/skills/import \
-H 'Content-Type: application/json' \
-d '{"url": "https://skills.sh/anthropics/skills/pdf"}'Returns HTTP 451 if the upstream license isn't OSS; re-POST with "accept_license": true to confirm.
Full pattern doc + reference implementations: docs/SKILLS_IMPORT.md.
🔍 Tools & skills tab — search + collapsible categories
The Tools tab in Settings used to be a flat list. As the skill ecosystem grows (built-ins + user-created + imported), that flat list becomes unscannable. New layout:
- Search box at the top — filters across tool name, description, and category
- Collapsible category headers — Memory / Files / Web / Browser / Hardware / Skills / Meta. Expand only what you need.
- Import skill button in the header — paste URL, install in one step.
The user-created and imported skills appear in their own categories so you can tell where each tool came from at a glance.
🐛 Notable fixes
-
fix(agent)— tool_call argument normalization. Some models (notably Qwen 3 variants) emittool_callswith already-stringified-but-invalid JSON inarguments(single quotes, trailing commas). Replay through_history_with_tool_callswould crash withJSONDecodeErrorand break the turn. Now normalized to valid JSON before replay. -
fix(canvas)— cross-thread leak + tool confusion. The model couldn't "read forms back" because_pending_canvas_renderswas a module global keyed by request_id only — concurrent threads would step on each other. Now bucketed by thread_id. -
fix(canvas)— stale server message. If you reload qwe-qwe after upgrading the...