Releases: PleasePrompto/ductor
v0.15.0
ductor v0.15.0
Multi-language UI, smart heartbeat system, cron routing, image processing, and a bunch of quality-of-life improvements.
TL;DR
- 7 languages — EN, DE, NL, FR, RU, ES, PT. Switch with one config line, hot-reloadable.
- Heartbeat per group/topic — independent schedules, custom prompts, per-target quiet hours. Each target runs its own loop.
- Cron results route back — jobs remember where they were created and deliver results there, not broadcast.
- Images auto-convert to WebP — resize + compress incoming photos, no more Claude API dimension errors.
/modelnever blocks — switch models while the agent is working.- Seen indicator + technical footer — optional read receipt emoji and token/cost info on every response.
- Transport-aware delivery — Telegram and Matrix messages stay on their transport. Cascading fallback when a transport is unavailable.
Multi-Language Support
Bot UI (commands, status messages, onboarding) is now available in 7 languages.
| Code | Language |
|---|---|
en |
English (default) |
de |
Deutsch |
nl |
Nederlands |
fr |
Français |
ru |
Русский |
es |
Español |
pt |
Português |
{"language": "de"}Hot-reloadable — change without restart.
Heartbeat Per Group/Topic
Heartbeat is no longer limited to private chats. Configure independent heartbeats for any group or forum topic, each with its own prompt, interval, and quiet hours.
{
"heartbeat": {
"enabled": true,
"interval_minutes": 30,
"group_targets": [
{
"enabled": true,
"chat_id": -1001234567890,
"topic_id": 5,
"prompt": "Check server status and report anomalies.",
"interval_minutes": 5,
"quiet_start": 23,
"quiet_end": 7
}
]
}
}- Each target with a custom
interval_minutesruns its own independent asyncio loop enabled: falsepauses a target without removing it- Chat validation with 1-hour TTL cache before firing
- Delivery fallback: if the target chat/topic is gone, the result goes to your main private chat
- The whole heartbeat system can be started/stopped via hot-reload (no restart needed)
Cron Result Routing
Cron jobs now remember where they were created and send results back there.
- Jobs created in a group topic → results go to that topic (UNICAST)
- Jobs without routing context → broadcast to all users (legacy behavior, backward compatible)
- Delivery failure → fallback to main user private chat with explanation
/cronoverview now shows the routing target per jobenabled: falseis now properly respected — disabled jobs don't execute or reschedule
Environment variables DUCTOR_CHAT_ID, DUCTOR_TOPIC_ID, and DUCTOR_TRANSPORT are auto-captured by cron_add.py.
Image Processing
Incoming images are automatically resized and converted to WebP after download. No more Claude API "exceeds dimension limit" errors from phone cameras.
- Max 2000px (configurable), WebP output (configurable), quality 85 (configurable)
- Works across all transports: Telegram, Matrix, API
- Errors are non-fatal — original file used as fallback
- New dependency: Pillow (installed automatically)
{
"image": {
"max_dimension": 2000,
"output_format": "webp",
"quality": 85
}
}/model Never Blocks
The /model selector now opens instantly, even while the agent is processing. The busy-info ("process still running — new model applies to next message") only appears after you actually switch, not when opening the selector.
- Topic-aware: switching in Topic A doesn't affect Topic B
ProcessRegistrynow trackstopic_idfor per-topic busy checks
Seen Indicator & Technical Footer
Two opt-in features, both default off:
- Seen indicator — 👀 emoji reaction on incoming messages (Telegram) or read receipt (Matrix)
- Technical footer — model name, token count, cost, and duration appended to every response
{
"scene": {
"seen_reaction": true,
"technical_footer": true
}
}Both are transport-agnostic (defined in BotProtocol) and hot-reloadable.
Transport-Aware Delivery
The MessageBus now routes UNICAST envelopes to the correct transport only. When Telegram and Matrix run in parallel, a heartbeat for a Telegram group stays on Telegram.
TransportAdapterdeclares itstransport_name("tg"/"mx")- Cascading fallback: target transport unavailable → other transport gets the message with explanation
DUCTOR_TRANSPORTenv var propagated to CLI subprocesses
Tool Activity: Shell Label
Tool activity now shows [TOOL: Shell] instead of [TOOL: Bash] — platform-neutral across Linux, macOS, and Windows. Applied to Telegram, Matrix, and API transports.
Bug Fixes
- Media ignores
group_mention_only(#57) — media messages in groups now respect the config flag - UpdateObserver on all agents (#58) — only the main agent runs the update checker
- Cron reschedule kills running jobs (#56) — executing jobs survive
cron_jobs.jsonchanges - Heartbeat not delivered to topic —
topic_idnow passed asthread_idto Telegram - Technical footer missing in streaming — footer is appended as text segment before finalize
Documentation
- All 13 docs files updated for new features
- RULES files (CLAUDE.md/AGENTS.md/GEMINI.md) updated with all config sections
config.example.jsonupdated withlanguage,image,scene, andheartbeat.group_targets- README updated with language support and feature list
Hot-Reloadable Settings
These settings now take effect within seconds — no restart needed:
model, provider, streaming, heartbeat, cleanup, scene, image, language, allowed_user_ids, allowed_group_ids, group_mention_only
v0.14.0
ductor v0.14.0
Matrix support, modular messenger architecture, and the /interrupt command.
Matrix Transport
ductor now runs on Matrix alongside Telegram. Use it as your only transport or run both in parallel — same agent, same workspace, same sessions.
- Segment-based streaming — responses arrive as separate messages, clean and readable
- Reaction buttons — emoji digits (1️⃣–🔟) replace Telegram's inline keyboards, with text input fallback
- Incoming media — images, audio, video, and files are downloaded and injected into the conversation
- Full command parity — every command works with both
!cmdand/cmdprefix - Auth model — room allowlist + user allowlist + group_mention_only support
- Auto-join/leave — invited to an allowed room? Joined. Unauthorized? Rejected and left.
ductor install matrix # install matrix-nio dependencyThen add to config:
{"transports": ["telegram", "matrix"]}Setup guide: docs/matrix-setup.md
Modular Messenger Architecture
The entire bot layer has been restructured into a transport plugin system. The core (orchestrator, sessions, CLI, cron, webhooks) never knows which messenger delivered the message.
BotProtocol— runtime-checkable interface that every transport implementsTransportRegistry— factory dict for bot creation, lazy importsMultiBotAdapter— runs multiple transports in parallel on a shared orchestratorSessionKey— transport-prefixed persistence keys (tg:123vsmx:456) prevent session collisionsMessengerCapabilities— feature matrix per transport (streaming style, button support, etc.)
Adding a new messenger (Discord, Slack, Signal, ...) means implementing BotProtocol in a new sub-package. Guide: docs/modules/messenger.md
/interrupt — Soft Interrupt
New command for when you don't want to nuke everything.
| Command | What it does |
|---|---|
/interrupt |
Sends SIGINT to the current process. Queued messages continue. |
/stop |
Kills the current process and discards all queued messages. |
/stop_all |
Kills everything — all messages, sessions, tasks, all agents. |
Trigger words: esc, interrupt, skip, überspringen
Works across Telegram (private, groups, topics), Matrix rooms, sub-agent chats, and named sessions.
Bug Fixes
- Bot no longer responds to
/command@other_bot(#36) — in group chats with multiple bots, commands addressed to another bot are now correctly ignored - Codex Docker stdin on Windows (#40) —
docker exec -ikeeps stdin open so prompts reach the CLI - Docker container paths (#38) —
/ductor/...paths from inside the container are translated to host paths for file sending - Matrix message replay — sent events are tracked to prevent duplicate processing after reconnect
- Matrix
/stop_all— now correctly interrupts across all transports, not just the current one
Documentation
- New Matrix setup guide with step-by-step instructions
- Group setup tip: how to use
/whereto find and add group IDs - Expanded
config.example.jsonwith all features - Updated all module docs for the new messenger architecture
Community
- PR #29 by @n-haminger — Matrix transport implementation. Merged and restructured into the modular plugin system.
- PR #37 by @liuh886 — Group command filtering and Windows Docker fixes. Ported into the restructured codebase.
Upgrade
ductor upgrade
# or
pipx upgrade ductorv0.13.0
ductor v0.13.0
Docker sandbox extras, external API secrets, and a round of cross-platform fixes.
Docker Extras — AI/ML Packages in Your Sandbox
Your Docker sandbox can now run AI/ML workloads. Want your agent to transcribe audio, analyze images, or scrape websites? Pick the packages you need during onboarding — or add them later with a single command.
Available extras: Whisper (speech-to-text), PyTorch CPU (machine learning), OpenCV (image/video processing), Tesseract (OCR), EasyOCR (multi-language OCR), Playwright (browser automation), yt-dlp (media download).
- Pick during setup — the onboarding wizard lets you choose which extras to install
- Add or remove anytime —
ductor docker extras-add whisper,ductor docker extras-remove whisper - Smart dependencies — selecting EasyOCR automatically includes PyTorch, no manual juggling
- Stays lightweight — PyTorch is pinned to CPU-only (~200 MB instead of ~900 MB with CUDA), keeping your image lean
- See what's happening — image builds stream progress to your terminal so you're never staring at a blank screen
ductor docker extras-add whisper # adds whisper + dependencies
ductor docker extras-list # show installed extras
ductor docker rebuild # rebuild image with new extrasExternal API Secrets (~/.ductor/.env)
A single file for all your external API keys. Your agents can use OpenAI, custom services, or any API that needs a token — just drop the key in ~/.ductor/.env and every CLI session picks it up automatically.
- No restart needed — edit the file while the bot runs, your next message already uses the new keys
- Safe defaults — your system environment variables always take priority, nothing gets accidentally overwritten
- Works everywhere — Linux, macOS, Windows, with and without Docker
# ~/.ductor/.env
OPENAI_API_KEY=sk-...
CUSTOM_SERVICE_TOKEN=abc123Bug Fixes
- Models don't randomly disappear anymore (#34) — if model discovery fails (network issue, API down), the bot now falls back to its last known list instead of showing zero models
- Windows + Docker works (#33) — fixed path translation and a startup crash on Windows
- Gemini actually reads input (#33) — removed a flag that silently blocked Gemini from reading prompts
- Forum topic replies land in the right place (#31) — async sub-agent results now arrive in the topic you started them from, not somewhere random in the group
- Gemini fallback cleanup — reconciled community PR with our cache architecture for a cleaner codebase
Documentation
- CLI agents now know how to restart the bot on their own (
touch ~/.ductor/restart-requested) - Full
.envdocumentation across config, infra, and workspace docs
Community
PR #35 by @liuh886 — Windows Docker support, Gemini model defaults, and session stability fixes. Merged and extended with our own improvements on top. Thank you!
Upgrade
ductor upgrade
# or
pip install --upgrade ductorv0.12.0
Forum Topics — Multiple Chats in One Group
ductor now fully supports Telegram forum topics. Create a group, enable topics, and each topic becomes its own isolated chat with its own conversation context.
- Topic isolation — every topic has its own session, independent from General and your private chat
- Per-topic model selection — run
/modelinside a topic to change just that topic's provider - Background tasks stay in their topic — task questions and results are delivered back to the topic they were started in
- Named sessions work in topics —
/sessioncreates sessions scoped to the current topic - Topic names in logs and status — topics show their name instead of just an ID
A group with 5 topics gives you 6 independent conversations (General + 5 topics), all sharing the same workspace, tools, and memory.
Group Chat Management
/where— see all tracked groups (active, rejected, left) with their IDs/leave <group_id>— manually leave a group- Auto-leave — bot auto-leaves groups not in
allowed_group_ids - Group audit — runs on startup, on config reload, and every 24 hours
allowed_user_idsandallowed_group_idsare now hot-reloadable — edit config.json, changes apply within secondsgroup_mention_onlyis now hot-reloadable — no restart needed to toggle
Background Tasks
- Task results now include the actual result — previously, the final task result injection could send the original prompt instead of the computed answer. Fixed.
- TASKMEMORY.md content appended to results — when a task completes, its memory file is included so the parent agent gets the full picture
- Resume reminders — task agents get a short reminder on each follow-up about how to communicate (ask_parent, TASKMEMORY)
- Delete tasks — new
delete_task.pytool lets models permanently remove finished tasks (entry + folder) - Sub-agent folder cleanup — fixed a bug where sub-agent task folders were not cleaned up on removal
Timeout Recovery (PR #24 by @n-haminger)
- Session preserved on CLI timeout — when the CLI times out, the session ID is now captured early and preserved so the next message auto-resumes where it left off
- Clear user message — shows timeout duration and confirms the session is preserved
Inter-Agent Communication (PR #25 by @n-haminger)
--summaryflag for async messages —ask_agent_async.py --summary "Short description"lets the sender control what preview text the recipient sees in their Telegram notification
Internal Improvements
- Unified MessageBus with Envelope system — all background delivery (cron, webhooks, heartbeat, tasks, inter-agent) now routes through a single bus
- Major orchestrator refactoring — extracted selectors, providers, and lifecycle into separate modules
- Centralized shared code, eliminated duplication across modules
- Comprehensive documentation rewrite
Full Changelog: v0.11.0...v0.12.0
ductor v0.11.0
ductor v0.11.0
Background tasks, timeout resilience, startup recovery, and a bunch of fixes. The biggest release since launch — 88 files changed, ~6800 lines added.
Background Tasks
Every chat — main or sub-agent — can now delegate long-running work to background tasks. You keep chatting while the task runs autonomously.
The agent decides on its own when to delegate (anything likely taking >30 seconds), but you can also tell it explicitly. When a task finishes, its full result flows back into your chat context — as if the agent had done the work itself.
You: "Research the top 5 competitors and write a summary"
→ Agent delegates this to a background task automatically
→ You keep chatting: "While that's running, explain our pricing model"
→ Task finishes → result delivered into your conversation
You: "Delegate this: generate PDF reports for all Q4 metrics"
→ Explicitly delegated — task starts, you keep chatting
→ Task has a question? It asks the agent, agent asks you, you answer, task continues
/tasks → view/manage all background tasks
Each task gets its own memory file (TASKMEMORY.md) in the workspace and can be resumed with follow-up prompts. Tasks are isolated per agent — a sub-agent's tasks live in its own workspace with its own provider.
Timeout Controller
Long-running CLI streams no longer die silently at the timeout boundary.
- Staged warnings at T-60s and T-10s so you know what's coming
- Activity-based extension — if the CLI is still producing output, the timeout extends automatically (configurable max)
- User-facing messages in Telegram for all timeout events
Startup Recovery
When the bot restarts (service restart, reboot, crash), interrupted work is automatically recovered.
- Boot ID tracking — detects restart vs. reboot
- Inflight turn persistence — tracks what was running when the bot stopped
- Recovery planner — resumes interrupted foreground turns and named sessions
- Lifecycle notifications — Telegram shows "Service restarted" / "Reboot detected"
Fixes
- Internal API startup failure propagation — if the inter-agent API can't bind its port, startup fails immediately instead of continuing in a broken state (#21, thanks @clawinbox24!)
- Gemini 3.1 models visible — removed
isActiveModelfilter that hid preview models likegemini-3.1-pro-preview(#22) - Gemini model cache refresh on startup — models are now always rediscovered on bot restart (matching existing Codex behavior)
- Task rules for Gemini —
ask_parent.pyinstructions are now more prominent so Gemini CLI agents actually use the tool instead of writing questions as text - Session recovery status — stores correct "idle" status instead of "running"
- Chat-ID resolution — happens before parallel-limit check in TaskHub
- Per-agent CLI binding — sub-agent tasks use the correct provider
- Agent isolation — task list/cancel/resume API endpoints respect agent boundaries
Documentation
- README rewrite — sessions, background tasks, and sub-agents each get their own section with examples and a comparison table
- Updated docs across all modules for new features
Community
This release includes work inspired by two community pull requests:
- #20 by @jhste102lab — timeout resilience, restart visibility, and auto-resume recovery (changes adapted and integrated)
- #21 by @clawinbox24 — propagate internal API startup failures (merged directly)
Thank you both!
Stats
- 88 files changed, ~6850 insertions
- 2727+ tests passing
- mypy clean, ruff clean
Upgrade
ductor upgrade
# or
pip install --upgrade ductorductor v0.10.0
ductor v0.10.0
Async sub-agent results now flow back into the main agent's conversation — with full context.
How /agents works now
Each sub-agent is a full agent with its own Telegram chat — you can open it and talk to it directly, just like your main agent. It has its own workspace, memory, and provider configuration.
The new part: when the main agent delegates a task to a sub-agent, the result flows back into your main conversation with full context. Direct interaction with a sub-agent stays in its own chat.
Two ways to use sub-agents
1. Direct chat — open the sub-agent's Telegram bot and talk to it like any other agent. Own context, own conversation.
2. Delegation from main agent — the main agent sends a task, the result comes back into your main chat:
Synchronous delegation (blocking)
You: "Ask codex-agent to check the API tests"
→ Main agent calls sub-agent (blocks)
→ Sub-agent executes, returns result as tool output
→ Main agent processes result in the SAME conversation turn
→ Main agent responds to you with full context
Asynchronous delegation (non-blocking) — fixed in v0.10.0
You: "Give codex-agent a task: migrate the database schema"
→ Main agent fires off task, keeps chatting with you
→ Sub-agent works in the background (can take minutes)
→ Sub-agent finishes
→ Result is injected into the main agent's CURRENT active session
→ Main agent processes with full conversation context
→ Main agent reports the result to you
Before v0.10.0: The async result arrived in a new, context-less session. The main agent didn't know what you originally asked or why it delegated the task. It responded blindly.
Now: The result is injected into whatever session is currently active (even if you did /new or switched providers in the meantime). The prompt is self-contained — it includes the original task description and the sub-agent's response. The main agent always has enough context to give you a meaningful answer.
What happens under the hood
- When the main agent sends an async task, the full task message is preserved
- When the result arrives, the per-chat lock is acquired (no concurrent session access)
- The current active session is resolved via
get_active()— always the latest, not the original - A self-contained prompt is built: original task + sub-agent result + session hint
- The CLI resumes the current session with
--resume - Session metrics (cost, tokens, session ID) are updated after execution
- Telegram shows "typing..." while the main agent processes the result
/session vs /agents — when to use which
/session — independent conversations with their own context
Named sessions let you start separate conversations inside the same Telegram chat, each with its own context. They are completely independent from your main chat — the main agent doesn't know about them and they don't share context with it.
/session Fix the login bug → starts session "firmowl" (own context)
/session @codex Refactor the parser → starts session "pureray" on Codex (own context)
@firmowl Also check the tests → follow-up in firmowl's context
/sessions → list/manage all active sessions
/agents — full agents you can talk to directly or delegate to
Sub-agents have their own Telegram chat — use them directly for independent work, or let the main agent delegate tasks. Delegated results flow back into the main chat with full context.
# Direct chat (own context):
Open codex-agent's Telegram bot → "Refactor the parser module"
# Delegation (main chat context):
Main chat → "Ask codex-agent to write tests for the API module"
→ Result flows back into main agent's session
→ Main agent tells you the result
/session (Named Sessions) |
/agents (Sub-Agents) |
|
|---|---|---|
| What it is | Independent conversation with its own context | Full agent with its own Telegram chat |
| Context | Own context, independent from main chat | Own context in direct chat — delegation flows back into main chat context |
| Workspace | Same workspace as main agent | Own workspace, own memory |
| Provider | Any provider/model per session | Own default provider/model |
| Result | Telegram reply (standalone) | Direct chat: standalone. Delegation: injected into main agent's session. |
| Use case | Parallel side-conversations you manage yourself | Use directly or let the main agent orchestrate |
| Setup | None — just /session <prompt> |
ductor agents add <name> + BotFather token |
Rule of thumb: Use /session when you want a separate conversation on the side. Use sub-agents when you want a dedicated agent you can either talk to directly or let the main agent delegate to.
Technical changes
bus.py:AsyncInterAgentResultnow carriesoriginal_message— the full task text sent to the sub-agent, populated in all code paths (success, timeout, error)app.py:on_async_interagent_result()acquires the per-chat sequential lock and shows a typing indicator while the result is being processedcore.py:handle_async_interagent_result()resumes the current active session, builds a self-contained prompt, and updates session metrics after executiontest_interagent.py: Updated tests for new API + 2 new tests (original message in prompt, resume of active session)README.md: Added "Sessions vs. Sub-Agents" comparison table
Upgrade
ductor upgrade
# or
pip install --upgrade ductorductor v0.9.1
ductor v0.9.1
Hotfix for v0.9.0 — fixes a type error in /stop_all when background tasks are active.
Bug fix
/stop_allwith background tasks: Fixed aTypeErrorinabort_all_agents()that occurred when a sub-agent had active background tasks. The background task registry was iterated by task ID (string) instead of chat ID (int), causingcancel_all()to fail.
What's in v0.9.0
Named inter-agent sessions, /stop_all for multi-agent abort, provider-switch detection, and UX improvements.
Named inter-agent sessions
Inter-agent communication now uses persistent named sessions — sub-agents remember previous conversations and can resume where they left off.
- Automatic session resume: When the main agent talks to a sub-agent, the conversation continues in the same CLI session. No more cold starts on every message.
- Per-sender isolation: Each calling agent gets its own named session (
ia-main,ia-helper, etc.), so parallel conversations don't interfere. --newflag: Pass--newinask_agent/ask_agent_asyncto force a fresh session when you need a clean slate.- Session persistence: Named sessions survive bot restarts — stored in each agent's
named_sessions.json. - Provider-switch detection: If a sub-agent switches provider (e.g. codex → claude), the old session is automatically ended (session IDs aren't portable across providers) and a fresh one is created. A notification is sent to the Telegram chat so you know what happened.
Thanks to @n-haminger for the Named Sessions implementation in #18!
/stop_all — kill everything
New command that stops all running processes across all agents at once.
/stop_allcommand: Kills CLI processes on the main agent and every sub-agent, plus cancels in-flight async inter-agent tasks on the bus.- "stop all" bare words: Type "stop all", "stopp alle", "cancel all", or "abort all" in chat — works the same as the command, no slash needed.
- Multi-provider safe: Works regardless of how many sub-agents you have or which providers they use.
/stopstill only kills local processes — use/stop_allwhen you want to nuke everything.
UX improvements
- Case-insensitive commands:
/Session,/STATUS,/Modelall work now. Mobile keyboards that auto-capitalize after/no longer break commands. - Changelog keeps upgrade button: Tapping "Changelog" on an update notification no longer removes the "Upgrade now" button. Both stay visible, and "Upgrade now" is also shown below the changelog text.
- Clean command names: Fixed
/agent\_startdisplaying with a backslash in usage messages — now shows/agent_startcorrectly.
Upgrade
ductor upgrade
# or
pip install --upgrade ductorductor v0.9.0
ductor v0.9.0
Named inter-agent sessions, /stop_all for multi-agent abort, provider-switch detection, and UX improvements.
Named inter-agent sessions
Inter-agent communication now uses persistent named sessions — sub-agents remember previous conversations and can resume where they left off.
- Automatic session resume: When the main agent talks to a sub-agent, the conversation continues in the same CLI session. No more cold starts on every message.
- Per-sender isolation: Each calling agent gets its own named session (
ia-main,ia-helper, etc.), so parallel conversations don't interfere. --newflag: Pass--newinask_agent/ask_agent_asyncto force a fresh session when you need a clean slate.- Session persistence: Named sessions survive bot restarts — stored in each agent's
named_sessions.json. - Provider-switch detection: If a sub-agent switches provider (e.g. codex → claude), the old session is automatically ended (session IDs aren't portable across providers) and a fresh one is created. A notification is sent to the Telegram chat so you know what happened.
Thanks to @n-haminger for the Named Sessions implementation in #18!
/stop_all — kill everything
New command that stops all running processes across all agents at once.
/stop_allcommand: Kills CLI processes on the main agent and every sub-agent, plus cancels in-flight async inter-agent tasks on the bus.- "stop all" bare words: Type "stop all", "stopp alle", "cancel all", or "abort all" in chat — works the same as the command, no slash needed.
- Multi-provider safe: Works regardless of how many sub-agents you have or which providers they use.
/stopstill only kills local processes — use/stop_allwhen you want to nuke everything.
UX improvements
- Case-insensitive commands:
/Session,/STATUS,/Modelall work now. Mobile keyboards that auto-capitalize after/no longer break commands. - Changelog keeps upgrade button: Tapping "Changelog" on an update notification no longer removes the "Upgrade now" button. Both stay visible, and "Upgrade now" is also shown below the changelog text.
- Clean command names: Fixed
/agent\_startdisplaying with a backslash in usage messages — now shows/agent_startcorrectly.
Upgrade
ductor upgrade
# or
pip install --upgrade ductorductor v0.8.0
ductor v0.8.0
Multi-agent system, group chat mode, Docker sandboxing for multi-agent, and robust inter-agent communication.
Multi-agent system
Run multiple ductor agents in a single process — each with its own Telegram bot, provider config, and workspace, coordinated by a central supervisor.
- Single-process supervisor:
AgentSupervisormanages main + sub-agents as supervised asyncio tasks with automatic crash recovery (exponential backoff, max 5 retries). agents.jsonconfig: Define sub-agents with their own Telegram token, provider, model, and reasoning effort. Hot-reloaded via FileWatcher — add/remove agents without restart.- Inter-agent bus: In-memory
InterAgentBusfor sync and async messaging between agents. CLI tools useask_agent.py/ask_agent_async.pyvia the internal HTTP API. - Internal HTTP API: Localhost aiohttp server (
127.0.0.1:8799) bridges CLI subprocesses to the bus. Endpoints:/interagent/send,/interagent/send_async,/interagent/agents,/interagent/health. - Shared knowledge:
SHAREDMEMORY.mdat root is automatically synced into every agent'sMAINMEMORY.mdviaSharedKnowledgeSync. - Health monitoring: Live health status per agent (starting/running/stopped/crashed) with uptime, restart count, and last crash error. Exposed via
/interagent/healthandductor status. - Agent identity: Each agent gets its own
CLAUDE.md/AGENTS.md/GEMINI.mdwith injected identity (name, role, available peers). - CLI management:
ductor agentslists all sub-agents.ductor agents add <name>/ductor agents remove <name>for interactive management. - Telegram commands:
/agentslists running agents with health status./agent_restart <name>restarts a sub-agent in-process. - Ordered startup: Main agent starts first (Docker, workspace, auth), sub-agents start only after main is ready — no race conditions.
Thanks to @n-haminger for the initial multi-agent system implementation in #16!
Group mention-only mode
New group_mention_only config option for running ductor in Telegram group chats without responding to every message.
- Bot only responds when mentioned (
@botname) or replied to directly. - Works for both text and media messages.
- Enable via
"group_mention_only": trueinconfig.json.
Thanks to @n-haminger for #15!
Docker multi-agent support
Shared Docker container architecture for multi-agent mode — all agents use one sandbox.
- Single shared container: Root
~/.ductormounted at/ductor. Main agent creates the container; sub-agents reuse it via class-level asyncio lock (no race conditions). - Per-agent working directory:
docker exec -w /ductor/agents/<name>/workspaceensures each agent operates in its own workspace. - Container path mapping: Host paths are translated to container paths (
~/.ductor/agents/test/workspace→/ductor/agents/test/workspace). - Inter-agent communication in Docker:
--add-host=host.docker.internal:host-gatewayfor Linux DNS resolution.InternalAgentAPIbinds to0.0.0.0in Docker mode so containers reach the bus. - No rebuild needed: Existing Docker users just need
ductor stop && ductor— the image stays the same, only the container is recreated with correct flags.
Bug fixes
- Codex parser:
parse_codex_result()no longer leaks raw JSONL when Codex produces no assistant text (e.g. silent-success tasks). Differentiates between genuine empty output and unparseable output. - Skill sync race conditions:
_ensure_copyand_newest_mtimetolerate concurrent file operations from parallel agents (marker deletion,__pycache__repopulation, partial rmtree). - Lint/type fixes: Resolved ruff SIM102/SIM103/TRY300 warnings and mypy type narrowing issues across multiple files.
Upgrade
ductor upgrade
# or
pip install --upgrade ductorExisting Docker users:
ductor stop # removes old container
ductor # creates new container with correct multi-agent flagsductor v0.7.0
ductor v0.7.0
Named background sessions, @model shortcuts, config hot-reload, and a streamlined README.
Named background sessions
Background tasks are no longer fire-and-forget. /session creates named sessions with persistent CLI session IDs that support follow-up messages, provider isolation, and interactive management.
- Named sessions:
/session Fix the login bugstarts a named session (e.g. "firmowl") and delivers tagged results. - Provider targeting:
/session @codex Refactor the parserruns on a specific provider. - @model shortcuts:
/session @opus Analyze architectureresolves model-to-provider automatically (@opus= Claude,@flash= Gemini,@codex= Codex). - Foreground follow-ups:
@firmowl Also check the testsstreams a follow-up in the current chat. - Background follow-ups:
/session @firmowl Add error handlingqueues a background follow-up. - Session management:
/sessionsshows all active sessions with end/refresh buttons. - Button routing: Buttons in session results route back to the correct session.
- Per-label process control: Sessions can be individually stopped without killing the main chat.
Smart per-provider model resolution
Cross-provider sessions now correctly resolve models instead of leaking the default provider's model.
BackgroundSubmitcarriesprovider_overrideandmodel_overridethrough the full execution chain.CLIService._make_cli()separates the provider-override path from the normal model-resolution path.- Gemini sessions pass empty model (CLI auto-selects) instead of inheriting Claude's "sonnet".
- Auth-aware
/sessionhelp text shows only providers you have authenticated.
Config hot-reload
Edit config.json while the bot is running — hot-reloadable fields apply immediately without restart.
- Mtime-based watcher: 5-second poll on
config.json, same pattern as cron/webhook watchers. - Hot-reloadable fields:
model,provider,reasoning_effort,cli_timeout,max_budget_usd,max_turns,permission_mode,streaming.*,heartbeat.*,cleanup.*,cli_parameters.*, and more. - Restart-required fields:
telegram_token,allowed_user_ids,docker.*,api.*— logged as warnings. - Safe reload: Pydantic validation before applying. Failed reloads are logged and skipped.
Documentation overhaul
- README: Streamlined from 337 to 206 lines with Mermaid architecture diagram, focused structure, and named sessions documentation.
- 14 module docs synchronized with implementation changes.
- "Why ductor?" section: Explains the official-CLIs-only philosophy.
Other improvements
- Compact session names without hyphens for better mobile readability.
- Improved cron job display in
/cronselector. - CI quality gate with pre-commit config.
Upgrade
ductor upgrade
# or
pip install --upgrade ductor