Skip to content

Releases: deepfounder-ai/castor

v0.25.0 — v0.25.0 — MiniMax tool-calling, reliable Telegram streaming, Docker server package

Choose a tag to compare

@github-actions github-actions released this 15 Jun 20:18

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 a browser_open call 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 prior tool_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>answer across 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_entity looks 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/dedupe collapse 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 1M instead of 1000k, and falls back to the real settings dump instead of a dead state.settings reference. model_context is 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 live badge 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 CMD ran a non-existent qwe-qwe command (the console script is castor), and it never copied prompts/ or schemas/, so goals and presets crashed. Added a /data VOLUME, sane env defaults, and a /api/status HEALTHCHECK.
  • docker-compose.yml pulls the prebuilt GHCR image, bind-mounts ./castor-data:/data so all state (SQLite, Qdrant vectors, wiki, skills, uploads, presets, logs) survives restarts and upgrades, reads config from .env, and sets shm_size: 1gb so Chromium doesn't crash.
  • New .env.example (provider URL/model/key, CASTOR_PASSWORD web auth) and docs/DEPLOY.md with 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_v2 flag — v2 has been the only path in production. With it went the v1-only self-check cluster and the self_check_enabled setting.
  • 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, unused agent_budget limit fields, a dead scheduler._log_run branch, the SKILLS_DIR alias, 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

Choose a tag to compare

@github-actions github-actions released this 14 Jun 18:17

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 (![](url "caption") → photo / audio / video / GIF).
  • Live <tg-thinking> streaming — private chats now stream the agent's reasoning in an ephemeral "Thinking…" block (via sendRichMessageDraft, which also fixes the long-broken draft path that always failed with RANDOM_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_dir fixture leaked castor_pytest_* tempdirs on locked-Qdrant / crash teardown — one dev tree hit 8157 dirs / 24 GB. Now self-heals: startup sweep of stale dirs, a pytest_sessionfinish cleanup of this run's dirs, and Qdrant-close-before-rmtree.
  • Migration tests moved off tempfile.mkdtemp() (which leaked) to pytest's tmp_path, and their sqlite connections close via contextlib.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)

Choose a tag to compare

@github-actions github-actions released this 04 Jun 15:52

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_single scrubs by default; scrub=False opt-out works.
  • memory.save_save_single chain scrubs once, no double-warning.
  • save_message scrubs content / tool_calls (incl. fact_save shape) / meta.
  • save_message passes clean text byte-for-byte.
  • tool_start scrubs args dict, incl. fact_save keyed-as-secret value.
  • tool_end scrubs result_preview; empty-result safe.
  • _register_trajectory_prune is opt-in (skips when trajectory disabled).
  • _execute_task routes the task name to prune_old.
  • _is_routine returns False (system task stays on fast path).
  • prune_old actually deletes stale *.jsonl files.

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

Choose a tag to compare

@github-actions github-actions released this 02 Jun 22:24

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.00
  • capitulating_goals — goal status='done' with failed subtasks or no acceptance criteria
  • shell_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_routine fix)
  • 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 errors and 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_HOME is set — the user installed CUDA Toolkit intentionally, so the message switches to an informational "embeddings use CPU by default; GPU package is unused unless embed_device=cuda."
  • Skips the warning entirely when setting:embed_device = cuda is 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/reindex

To 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

Choose a tag to compare

@github-actions github-actions released this 28 May 01:29

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 guard
  • test_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

Choose a tag to compare

@github-actions github-actions released this 27 May 23:51

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.csv and routes it under the goal dir transparently.

  • Budget cap (budget_usd) actually works. Migration 015 adds agent_runs.goal_id and rolls up costs via a LEFT JOIN. Before this commit, goals.cost_usd was 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 paused with 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_at as 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 its done_condition evaluated anyway. The gate now correctly bypasses skipped entries.

  • Orchestrator anti-capitulation prompt rule. The "Knowing when you have ENOUGH" section in prompts/orchestrator.md used 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.py module. Patterns moved out of memory.py. scrub_text for free-form text, scrub_fact(key, value) adds a key-name heuristic — keys named password, 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, and db.attach_goal_output all pass values through the appropriate scrub before write. save_checkpoint also walks tool_calls[].function.arguments so 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_list to the browser subagent's tool whitelist + a new Credential handling section in prompts/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.0 since their agent_runs rows have no goal_id link. New goals get accurate per-goal cost tracking immediately.

  • A paused goal with retry_after_sec set is invisible to claim_next_goal until 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-only reason= 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 with goal_runner.run, backoff blocking immediate reclaim.
  • test_secret_scrub_goals.py (25) — every goal storage path scrubs, including attach_goal_output and tool_calls.arguments.
  • test_agent_runs.py (+5) — goal_id column persistence, get_goal_total_cost, get_goal / list_goals cost 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

Choose a tag to compare

@github-actions github-actions released this 19 May 14:18

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 (--once for tests, auto-start in web mode).
  • Orchestrator — breaks the goal into subtasks, dispatches subagents, tracks progress via structured facts.
  • 4 subagent typesresearch, 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 KEY badge + 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 + routingproviders.py auto-routes to the native adapter when the active provider is anthropic.

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_subagent and burn 80+ rounds driving a browser directly. Fixed via _ORCHESTRATOR_EXCLUDED_TOOLS blacklist.
  • Goal plan validation — error message listed wrong done_condition kinds; 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-col containers retain position across re-renders.
  • Failed goals UI — failed goals wouldn't open in detail view (!gR.value.error guard rejected them). Fixed to check gR.value.id.
  • Streaming tool results — reply event was wiping tool results accumulated during streaming. allStrings guard 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 7861

Four new migrations apply automatically on first boot. No config changes required. Telemetry consent unchanged.


v0.22.1 — v0.22.1 — Migration reliability fix

Choose a tag to compare

@github-actions github-actions released this 12 May 21:24

v0.22.1 — Migration reliability fix

  • fix(db): SQLite migration runner now executes statements one-by-one instead of via executescript(). This makes ALTER TABLE ADD COLUMN migrations idempotent: if scheduler._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 /resume for 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_at to agent_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}/budget and POST /api/routines/{id}/budget.
  • Migration 010 adds budget_usd_cap + budget_period_sec to
    scheduled_tasks. Pre-existing routines have no cap (default).

v0.19.0 — Cost tracking & per-session analytics

  • New agent_runs table replaces routine_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/threads extended with input_tokens / output_tokens / cost_usd / run_count; new GET /api/threads/{id}/runs,
    GET /api/analytics/period, GET /api/pricing/status,
    POST /api/pricing/refresh.
  • Migration 008 atomically replaces legacy routine_runs with the new
    agent_runs table.

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 — no window.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 panel
  • canvas_prompt(html, title?, timeout_s=300) — blocks until submit / close / timeout, returns user data as JSON
  • canvas_save(slug, title?, html?) — persist as artifact
  • canvas_load(slug) — reopen a saved artifact
  • canvas_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 ...

Read more

v0.22.0 — v0.22.0 — Auto-resume after interrupt

Choose a tag to compare

@github-actions github-actions released this 12 May 20:45
5537162

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 /resume for 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_at to agent_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}/budget and POST /api/routines/{id}/budget.
  • Migration 010 adds budget_usd_cap + budget_period_sec to
    scheduled_tasks. Pre-existing routines have no cap (default).

v0.19.0 — Cost tracking & per-session analytics

  • New agent_runs table replaces routine_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/threads extended with input_tokens / output_tokens / cost_usd / run_count; new GET /api/threads/{id}/runs,
    GET /api/analytics/period, GET /api/pricing/status,
    POST /api/pricing/refresh.
  • Migration 008 atomically replaces legacy routine_runs with the new
    agent_runs table.

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 — no window.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 panel
  • canvas_prompt(html, title?, timeout_s=300) — blocks until submit / close / timeout, returns user data as JSON
  • canvas_save(slug, title?, html?) — persist as artifact
  • canvas_load(slug) — reopen a saved artifact
  • canvas_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.

...

Read more

v0.21.0 — v0.21.0 — Per-routine budget caps

Choose a tag to compare

@github-actions github-actions released this 12 May 20:39
cf831f0

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}/budget and POST /api/routines/{id}/budget.
  • Migration 010 adds budget_usd_cap + budget_period_sec to
    scheduled_tasks. Pre-existing routines have no cap (default).

v0.19.0 — Cost tracking & per-session analytics

  • New agent_runs table replaces routine_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/threads extended with input_tokens / output_tokens / cost_usd / run_count; new GET /api/threads/{id}/runs,
    GET /api/analytics/period, GET /api/pricing/status,
    POST /api/pricing/refresh.
  • Migration 008 atomically replaces legacy routine_runs with the new
    agent_runs table.

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 — no window.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 panel
  • canvas_prompt(html, title?, timeout_s=300) — blocks until submit / close / timeout, returns user data as JSON
  • canvas_save(slug, title?, html?) — persist as artifact
  • canvas_load(slug) — reopen a saved artifact
  • canvas_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) emit tool_calls with already-stringified-but-invalid JSON in arguments (single quotes, trailing commas). Replay through _history_with_tool_calls would crash with JSONDecodeError and 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_renders was 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...

Read more