Skip to content

v0.16.0

Choose a tag to compare

@PleasePrompto PleasePrompto released this 21 Apr 10:46
· 81 commits to main since this release

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).
  • /stop actually stopscancel_task now kills the provider CLI subprocess tree in under 2 seconds, not just the asyncio wrapper.
  • Claude Code 1M contextopus[1m] and sonnet[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 interactive bypasses max_parallel, --priority batch queues normally.
  • Notification routing — startup + upgrade events honor group_targets like 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_ID is now plumbed into nested task AgentRequest.
  • TaskResult carries 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_prompt is now persisted across process restarts.
  • TASKMEMORY.md truncation 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 the max_parallel cap; 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-to threads the reply to a specific message.
  • --silent suppresses 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.0

Gemini 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 via just 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-leaveallowed_channel_ids now 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.4 directive 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 ductor

Your ~/.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 @Revisorjustfile with fast dev commands, uv.lock for reproducible installs, opt-in pytest-xdist parallel test lane.
  • PR #98 by @viiccwen — Codex gpt-5.4 model 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.4 directive 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 BaseCLI extension path).
  • #102 Local LLMs via codex/claude (needs UI work around /model selector + 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