v0.16.0
ductor v0.16.0
Memory subsystem, task-runtime hardening, Claude 1M-context, emoji status tracker, and a polished backlog-zero pass across 28 issues and 6 contributor PRs.
TL;DR
- Memory that learns — pre-compaction flush, periodic reflection hook, LLM-driven summarization (no more truncate-and-discard).
/stopactually stops —cancel_tasknow kills the provider CLI subprocess tree in under 2 seconds, not just the asyncio wrapper.- Claude Code 1M context —
opus[1m]andsonnet[1m]selectable in/model, route with beta header. - Emoji status reactions by default — your message gets a live reaction that cycles thinking → tool → system → cleared while the agent works.
- Task priority lanes —
--priority interactivebypassesmax_parallel,--priority batchqueues normally. - Notification routing — startup + upgrade events honor
group_targetslike heartbeat already did. - Gemini thought leaks fixed —
[Thought: true]markers no longer reach Telegram chat. - Onboarding survives Windows — individual CLI auth probe failures no longer abort the wizard.
- 7 contributor PRs merged, 25 issues closed, 3 deferred to the next milestone.
Memory Subsystem
Three composable features around CompactBoundaryEvent and MessageHookRegistry.
Pre-compaction silent flush (#77)
When Claude Code signals it's about to compact the context window, ductor runs a silent turn that captures the state into MAINMEMORY.md before the drop.
{
"memory_flush": { "enabled": true }
}On by default. Disabling keeps the old behavior (compaction happens blind).
Memory reflection hook (#65)
A periodic "memory review" turn fires at a configurable cadence — think of it as an automated /memory compact that the agent decides to run itself.
{
"memory_reflection": {
"enabled": false,
"cadence_messages": 20
}
}Opt-in (default off). Turn cadence is per-chat.
LLM-driven compaction (#80)
Builds on the flush hook: when the context approaches its limit, ductor asks the same running provider to summarize MAINMEMORY.md into a compressed form, rather than truncating. No new CLI dependency — the same CLIService does the summarize turn.
{
"memory_compaction": { "enabled": true }
}On by default.
Task Runtime Hardening
Seven bug fixes that make background tasks and sub-agent delegation behave deterministically across restarts and cancellations.
/stop kills the subprocess (#92)
TaskHub.cancel / cancel_all now call ProcessRegistry.kill_for_task(task_id) before cancelling the asyncio task — because cli.execute was blocked on the subprocess pipe, CancelledError could never propagate until the pipe closed. Uses the existing SIGTERM → 2s → SIGKILL ladder.
Regression test spawns a real sleep 30 subprocess and asserts it exits within 3 seconds of kill_for_task — the mock-only test suite couldn't catch this bug class before.
Exit code 143 / 137 (SIGTERM/SIGKILL after /stop) is now classified as cancelled, not failed — the task status in /tasks reflects user intent instead of looking like a CLI error.
Background tasks inherit cli_parameters (#85)
Previously, cli_parameters.claude / .codex / .gemini set in config.json were dropped for cron / webhook / named-session task spawns. MCP config keys never reached the provider subprocess. Fixed: resolve_cli_config reads the correct per-provider bucket and concatenates with task overrides, mirroring the foreground orchestrator pattern.
Sub-agent task notifications route home (#73, #74)
DUCTOR_TOPIC_IDis now plumbed into nested taskAgentRequest.TaskResultcarries the originating sub-agent identity — results route back to the sub-agent that launched the task, not the main agent.
/new resets to config default (#82)
/new previously persisted the last-used model across sessions. Now it re-reads config.model on reset, matching the documented contract.
InterAgentBus stale-session recovery (#81)
InterAgentBus.send_async no longer fails silently when the named-session ID is stale (after a CLI cache clear or update). It detects SessionNotFound-class errors, falls back to a fresh session, and emits a visible warning.
Tool-only turn guard (#84)
When an agent turn contains only tool calls (no user-visible text), the bot now emits a neutral status reply instead of an empty message. Strategic memory-design fix belongs to Phase 4; this is the tactical guard.
Task persistence (#90, #91)
TaskEntry.original_promptis now persisted across process restarts.TASKMEMORY.mdtruncation at 4000 chars emits an explicit warning log line (was silent before).
Provider Support
Claude Code with 1M context (#76)
opus[1m] and sonnet[1m] appear in the /model selector and route via Claude CLI with the 1M-context beta header. Pure pass-through — Claude CLI strips the [1m] suffix and sets the header internally.
Gemini reasoning / thought filter (#110)
[Thought: true] markers from Gemini CLI (leaking from the bundled chunk-EA775AOR.js::toPart2 thought-wrapper) are now filtered out of the NDJSON parser boundary and routed into the existing provider-neutral ThinkingEvent. No more "Thought: true" text polluting Telegram chat.
Works regardless of the user's Gemini settings.json or CLI version.
DX Polish
Emoji-reaction status tracker (#63)
Your message now gets a live reaction that reflects the agent's work stage and clears on completion.
{
"scene": { "status_reaction": true }
}Default on. Set to false for silent operation.
Stages: thinking 🤔 → tool ⚡ → system → cleared. ReactionTracker swallows exceptions (graceful degradation — never blocks a turn on a reaction API failure).
Notification routing for startup + upgrade (#64)
startup_targets and upgrade_targets mirror the heartbeat.group_targets pattern. Route bot-lifecycle events to specific chats/topics with per-target enable flags.
{
"notifications": {
"startup_targets": [{ "enabled": true, "chat_id": -1001234567890, "topic_id": 5 }],
"upgrade_targets": [{ "enabled": false, "chat_id": null, "topic_id": null }]
}
}Matrix parity: startup notifications work on Matrix rooms too.
Configurable transcription command (#66)
External audio/video transcription via environment variable hand-off to a user-provided binary.
{
"transcription": {
"audio_command": "whisper --model small {input}",
"video_command": ""
}
}Unset keys fall back to the bundled transcription strategy — existing users see zero change.
Task priority lanes (#79)
create_task.py agent-tool now accepts --priority {interactive|batch}.
interactive— bypasses themax_parallelcap; active interactive tasks are excluded from the count.batch— queues normally (default).
No preemption, no anti-starvation — intentionally minimal.
Inter-agent message flags (#86)
ask_agent_async.py agent-tool now accepts --reply-to <message_id> and --silent.
--reply-tothreads the reply to a specific message.--silentsuppresses the user-facing notification while still delivering the message.
Onboarding & CLI
Non-fatal CLI auth-check (#109)
ductor onboarding no longer aborts if one of the three auth probes (claude / codex / gemini) raises. Instead: yellow-panel warning with the exception text, wizard continues as long as at least one provider is authenticated.
Also hardened check_codex_auth against Path.home() RuntimeError on Windows when %USERPROFILE% / %HOMEDRIVE% / %HOMEPATH% are all unset.
Follow-up known: the user-reported
OSError: 10048(port 8799 already bound) on Windows is a separate root cause — tracked as a Phase-next issue.
--version / -V prints version
Previously ductor --version fell through to the default action and started the bot. Now it prints the version and returns.
$ ductor --version
ductor 0.16.0Gemini auth-mode hot refresh
When you flip ~/.gemini/settings.json from gemini-api-key to oauth-personal (or vice versa), ductor now re-reads the setting on the next Gemini turn instead of forcing a restart.
Infrastructure
- i18n completeness checker wired into
just check— 8 locales (now including Indonesian), zero-gap enforcement in CI. justfile+uv.lock— fast dev commands, reproducible builds. Opt-in parallel tests viajust test-parallel.- Zero-warnings baseline — all pre-existing ruff + mypy warnings eliminated.
- Matrix transport parity — dedup, message queue, drain-on-stop, startup lifecycle + recovery.
- Telegram channel whitelist + auto-leave —
allowed_channel_idsnow filters incoming channel messages. - Gemini model discovery from prebundled CLI chunks — works with Gemini CLI v0.38.x bundled architecture.
- Cron hardening — double-execution fix, wall-clock drift protection,
@gpt-5.4directive routing.
Upgrade
Users on the yanked PyPI v0.15.0 wheel (which had a SyntaxError crash-loop in cron_sanitize.py — #99): pip install --upgrade ductor (or pipx upgrade ductor).
ductor upgrade
# or
pipx upgrade ductorYour ~/.ductor workspace, config, sessions, tasks, and memory files are preserved across the upgrade. New config sections (memory_flush, memory_reflection, memory_compaction, notifications, transcription) are deep-merged with safe defaults on first load.
Community
Massive thanks to the contributors whose PRs landed in this release:
- PR #87 by @mukhayyar — Indonesian (
id) UI translation (8th locale). - PR #96 by @Revisor —
justfilewith fast dev commands,uv.lockfor reproducible installs, opt-inpytest-xdistparallel test lane. - PR #98 by @viiccwen — Codex
gpt-5.4model support and cron job UX improvements. - PR #101 by @asdigitos — Telegram channel whitelist with auto-leave and localized message.
- PR #103 by @junlov — Matrix transport parity: dedup, message-queue drain-on-stop, startup lifecycle + recovery.
- PR #105 by @attid — Gemini model discovery from prebundled CLI chunks (fixes Gemini CLI v0.38.x).
- PR #106 by @oscarsovino — Cron double-execution fix, wall-clock drift protection,
@gpt-5.4directive routing.
Additional thanks to everyone who filed issues that made this release possible — @alexeymorozua and others for the extensive task-runtime bug reports (#73, #74, #81, #82, #84, #90, #91), and the community for reporting #63, #64, #65, #66, #76, #77, #79, #80, #85, #86, #92, #99, #109, #110.
Deferred to the Next Milestone
- #78 Tiered memory architecture (XL scope, cites academic paper — spike first).
- #94 Copilot CLI support (5th provider — scope creep, will document
BaseCLIextension path). - #102 Local LLMs via codex/claude (needs UI work around
/modelselector + per-provider allow-lists; groups with #94 + PR #100). - PRs deferred: #89 (Linear integration — policy decision on third-party HTTP + secrets), #93 (React Vite dashboard — needs dedicated repo + E2E boundary fix), #95 (SessionKey int→str refactor — needs Slack scope + full migration), #100 (OpenCode CLI — needs 4 audit fixes + cross-AI review).
Full changelog: v0.15.0...v0.16.0