feat(kanban): auto-subscribe origin conversation on kanban_create tool - #27064
feat(kanban): auto-subscribe origin conversation on kanban_create tool#27064TheoLong wants to merge 6 commits into
Conversation
Profile configs (profiles/*/config.yaml) use the short form
'model: <name>' at the top with 'provider: copilot' at the
root. load_cli_config() failed to migrate the root provider in
that shape because the legacy-fallback check was
'if not defaults["model"].get("provider")', and the hardcoded
default puts 'auto' there — truthy, so the migration was skipped.
Result: every kanban worker subprocess (and any 'hermes -p <name>'
invocation) resolved provider to 'auto', hit AuthError on the
primary provider, and fell through to the configured fallback
chain. profiles/architect/logs/errors.log shows thousands of
'Primary provider auth failed' warnings throughout May 2026.
Fix: when 'model:' is written as a STRING (no provider slot in
the dict), treat the default 'auto' as unset so the root-level
'provider:' is honored. Dict-form 'model:' is unchanged — a
stale root-level provider is still ignored there.
When a user replies to a message in Discord, the agent now receives a structured pointer containing the message_id, channel_id, author, and quoted text (1500 chars instead of 500). The pointer also includes an inline hint to call discord.fetch_messages with around=<message_id> to pull surrounding context — useful when the user replies in a channel referencing an earlier exchange instead of starting a new thread. - gateway/platforms/base.py: add reply_to_channel_id and reply_to_author fields to MessageEvent. - gateway/platforms/discord.py: populate the new fields from message.reference (channel_id, resolved.author display/global/name). Falls back to current channel when the reference omits channel_id. - gateway/run.py: replace the truncated [Replying to: ...] seed with a structured pointer that names the message, author, channel, and surfaces the fetch_messages tool hint. Quote cap raised to 1500 with a truncation note pointing at the same tool. - tools/discord_tool.py: add 'around' snowflake param to fetch_messages (Discord supports around/before/after as mutually exclusive anchors). Update manifest + schema description so the agent knows when to use it.
Three independent layers shipped on main today silently corrupt a user's Hermes venv: the systemd unit template sets VIRTUAL_ENV explicitly, a gateway-service test asserts it must be present, and the subprocess env blocklist in tools/environments/local.py has no entry for it. The result is that any agent terminal call that runs 'uv sync', 'uv run --active', 'pip install', or 'poetry install' from a user project rebuilds $VIRTUAL_ENV against that project's pyproject.toml, wiping every Hermes runtime dependency. Reproduced 2026-05-16: 'cd ~/Git/fb-operator && uv run --active fb obs' removed ~/.hermes/hermes-agent/.venv (Python 3.11) and recreated it as Python 3.12 with only fb-operator's 11 deps. Gateway restart then ModuleNotFoundError: hermes_cli. Fix (defense in depth): 1. hermes_cli/gateway.py: drop both 'Environment="VIRTUAL_ENV=..."' lines from the systemd unit templates. Gateway invokes python by absolute path; sys.prefix resolves from the interpreter location without VIRTUAL_ENV being set. 2. tests/hermes_cli/test_gateway_service.py: flip the assertion to 'VIRTUAL_ENV not in unit' with an explanatory comment. 3. tools/environments/local.py: add VIRTUAL_ENV, VIRTUAL_ENV_PROMPT, UV_PROJECT_ENVIRONMENT, POETRY_ACTIVE, PIPENV_ACTIVE, CONDA_PREFIX, CONDA_DEFAULT_ENV to _HERMES_PROVIDER_ENV_BLOCKLIST so the case where a user invokes hermes interactively with VIRTUAL_ENV set in their shell is also covered.
Revert behavior of d557544 locally — Theo's usage pattern wants both free-response (no @mention required) AND auto-thread per message. Upstream change suppressed auto-thread when is_free_channel was true, contradicting the long-standing local config where free_response_channels='*' relied on each conversation getting its own thread for isolation.
…itle Auto-title generation was Telegram-only; Discord threads got a session title saved to the DB but never had the thread itself renamed. Theo's workflow expects the thread title to reflect the conversation topic so threads stay scannable. Two changes: 1. gateway/run.py: add _is_discord_thread_lane, _rename_discord_thread_for_session_title, and _schedule_discord_thread_rename — parallel to the existing Telegram path. Uses the existing DiscordAdapter._client to fetch and edit the thread. Includes a 30s dedupe cache to avoid Discord rate-limit pressure when the title hasn't actually changed. 2. agent/title_generator.py: add maybe_retitle_session — fires every 6 user turns after the initial auto-title. Compares against the current stored title and only triggers the rename callback when the title actually drifts. Cheap (background thread, skips if unchanged). The same title_callback wiring drives both the first-turn auto-title and the periodic retitle. For Discord this means: thread is renamed on first agent reply, then re-evaluated every 6 user turns and renamed if the topic has drifted.
The kanban_notify_subs table + gateway notifier already deliver terminal
events (completed/blocked/gave_up) every 5s to subscribed (platform,
chat, thread) tuples. The /kanban create slash command and the dashboard
toggle both wire this up. The kanban_create *tool* (used by orchestrator
and worker agents to fan out child cards) did not — so workspaces spawned
from a Discord thread had no way back to that thread without a per-thread
cron polling the board.
Now: at task-creation time, resolve the originating conversation in two
steps and write notify subscriptions atomically with the task.
1. Live gateway session — gateway.session_context exposes the
platform/chat/thread the user is in via contextvars (mirrored to
HERMES_SESSION_* env). When present, that's the origin.
2. Parent task inheritance — when a kanban worker (HERMES_KANBAN_TASK
set) creates child cards, copy the parent's subs to the child.
The originating conversation thus stays the durable manager of
its whole subtree, not just the first hop.
Failures in auto-subscribe never break task creation. The notify_subs
UNIQUE (task, platform, chat, thread) constraint makes retries safe.
Tests cover: live Discord thread, channel-root (no thread), no-origin
CLI usage (silent), worker fan-out inheritance, live-session-wins-over-
parent, and idempotency on duplicate-create.
|
This re-implements the auto-subscribe feature that was merged in #19718, then reverted in #19721. The replacement approach merged in #19864 uses explicit per-platform dashboard toggles instead of auto-subscribe. Multiple competing PRs already exist for re-implementing the old approach: #25195 (open, primary), #24307 (open), #21523 (open). 12 files changed across kanban, discord, CLI, gateway — large blast radius for a feature that was intentionally reverted. |
|
I checked this against a live Telegram/orchestrator Kanban flow and can confirm the user-visible behavior described here. Observed situation:
Local evidence from the live instance:
User-facing impact: A Telegram user can ask the orchestrator in natural language to dispatch work, see the orchestrator accept/create the task, and then receive no completion notification when the Kanban worker finishes. From the user's perspective, the work appears to disappear unless they manually check the board or ask the orchestrator for status later. This is distinct from the slash-command path: tasks created with |
|
Thanks @TheoLong — closing this one. 620 LOC across 12 files including gateway/discord, title_generator, and cli.py is cross-cutting design territory. The auto-subscribe-on-kanban_create direction is similar to #24307 (closed) which re-introduces a previously reverted feature. We have gateway-create auto-subscribe today; if the gap from current behavior is concrete, please open an issue. Appreciate the work. |
What
The
kanban_createtool (used by orchestrator + worker agents) now auto-subscribes the originating conversation to terminal notifications, with parent-inheritance on worker fan-out. The originating thread becomes the durable manager of the whole subtree — no cron, no skill discipline, no manual `/kanban notify-subscribe`.Why
The notify infrastructure already exists (`kanban_notify_subs` table, gateway notifier polling every 5s, /kanban slash command and dashboard toggle both wire it up). What was missing: the tool path. So tasks spawned by an agent inside a Discord thread had no way to phone home unless someone manually subscribed or ran a per-thread cron.
How
In `tools/kanban_tools.py::_handle_create`, after `kb.create_task(...)`, call `_auto_subscribe_origin(kb, conn, new_tid)`. Two-step resolution:
Failures in auto-subscribe never break task creation (logged at DEBUG). The `UNIQUE(task, platform, chat, thread)` constraint on `kanban_notify_subs` makes the operation idempotent.
Tests
`tests/tools/test_kanban_tools.py` — 6 new cases:
All 64 `test_kanban_tools.py` + 16 notifier/notify-sub tests pass.
Out of scope