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 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 server, the JS knows about canvas tools but the server doesn't have the endpoint yet. We now show a clear "restart qwe-qwe" toast instead of a confusing 404.
📈 By the numbers
- +725 tests passing (was 545 at v0.18.6) — +180 new tests covering canvas + skill_import + their JS contracts
- 109 tests in
test_skill_import.pyalone — including a "live integration" path gated byRUN_LIVE_TESTS=1that fetches a real skills.sh skill - Coverage floor unchanged at 24% — actual 25.93%
- Two new SQLite migrations —
006_canvas_artifacts.sql,007_skill_imports.sql - Two new pattern docs —
docs/CANVAS.md,docs/SKILLS_IMPORT.md
⬆️ Upgrade
git pull
pip install -e . --upgrade
python cli.py --web --ssl --port 7861Two new migrations apply automatically on first boot. No config changes needed. Telemetry consent unchanged (no new event types).
🙏 Inspirations
Canvas takes obvious inspiration from Claude.ai's Artifacts — but the sandboxed-iframe-only approach matters more here, since qwe-qwe runs on your own machine and Anthropic doesn't sit between the LLM and your filesystem. Skill import works because Anthropic published the agentskills.io spec as a portable format — you can drop the same SKILL.md into Claude Code, Claude.ai, and qwe-qwe and it works in all three. The skills.sh catalog made discovery trivial.