Skip to content

feat(kanban): auto-subscribe origin conversation on kanban_create tool - #27064

Closed
TheoLong wants to merge 6 commits into
NousResearch:mainfrom
TheoLong:feat/kanban-tool-auto-notify-origin
Closed

feat(kanban): auto-subscribe origin conversation on kanban_create tool#27064
TheoLong wants to merge 6 commits into
NousResearch:mainfrom
TheoLong:feat/kanban-tool-auto-notify-origin

Conversation

@TheoLong

Copy link
Copy Markdown

What

The kanban_create tool (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:

  1. Live gateway session. `gateway.session_context.get_session_env()` reads HERMES_SESSION_PLATFORM / CHAT_ID / THREAD_ID from the contextvars set per-task by the gateway. When present, that's the exact (platform, chat, thread) the user is in — Discord thread, Telegram topic, Slack thread, etc.
  2. Parent task inheritance. When the creator is a kanban worker (`HERMES_KANBAN_TASK` set) with no live session, copy the parent task's subscriptions. This keeps the origin attached to every descendant when fan-out goes through multiple worker hops.

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:

  • Live Discord thread → subscribed
  • Channel-root message (no thread) → subscribed with empty thread_id
  • No origin signal at all → no sub, silent (correct)
  • Worker fan-out inherits parent's subs
  • Live session wins over parent inheritance
  • Duplicate auto-subscribe is idempotent

All 64 `test_kanban_tools.py` + 16 notifier/notify-sub tests pass.

Out of scope

  • Per-task opt-out flag on `kanban_create` (e.g. `notify_origin=False` for cards a noisy orchestrator wants silent). Easy to add later if fan-out noise becomes real.
  • Rolling fan-out children's events up into a parent summary instead of N separate pings. Same — additive when needed.

TheoLong and others added 6 commits May 16, 2026 13:46
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.
@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/tools Tool registry, model_tools, toolsets comp/gateway Gateway runner, session dispatch, delivery platform/discord Discord bot adapter labels May 16, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

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.

@impara

impara commented May 18, 2026

Copy link
Copy Markdown

I checked this against a live Telegram/orchestrator Kanban flow and can confirm the user-visible behavior described here.

Observed situation:

  • The user sends a natural-language request to the orchestrator Telegram gateway.
  • The orchestrator creates a Kanban task through the kanban_create tool path, not through the /kanban create ... slash-command path.
  • The task is created and dispatched normally.
  • The worker can complete the task and write the normal Kanban completion event.
  • The originating Telegram chat does not receive a completion reply/ping.

Local evidence from the live instance:

  • gateway/run.py has the Kanban notifier watcher that sends terminal events from kanban_notify_subs.
  • hermes_cli/kanban_db.py writes the completed task event with the handoff summary when a task completes.
  • /kanban create ... in the gateway path auto-subscribes the originating chat by adding a kanban_notify_subs row.
  • tools/kanban_tools.py::_handle_create creates tasks for the kanban_create tool path, but on this checkout it does not add a notify subscription.
  • The local board database showed kanban_notify_subs count 0 after a natural-language orchestrator-created task was spawned.
  • The task event log for that task contained created, claimed, and spawned events, but no subscription row existed for the Telegram origin, so there was no route for the notifier to deliver the eventual terminal event.

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 /kanban create ... are auto-subscribed to the originating gateway chat and are expected to send terminal-event pings back.

@teknium1

Copy link
Copy Markdown
Contributor

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery comp/tools Tool registry, model_tools, toolsets P3 Low — cosmetic, nice to have platform/discord Discord bot adapter type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants