Follow-up review fixes on the salvage of #54872 (原作者 张满良/@zmlgit):
1. [HIGH] Adapter selection now goes through the shared
_authorization_adapter chokepoint (gateway/authz_mixin.py) instead of a
local inline lookup that fell back to the DEFAULT profile's same-platform
adapter when the owning profile had a registry entry but no adapter for
that platform. That fallback re-introduced the exact cross-profile
mis-delivery ([230002] Bot can NOT be out of the chat) this change exists
to fix. Adds a mutation-verified guard test
(test_notifier_owning_profile_adapter_no_default_fallback).
2. [HIGH→documented] The creator-wake SessionSource cannot faithfully
reconstruct a DM/thread creator's session key because chat_type is neither
persisted on the subscription nor carried on the session-context bridge.
Documented the limitation inline; behavior degrades to a fresh group
session (never an exception). The end-to-end fix (stamp + persist
chat_type) is a scoped follow-up, not bundled into this salvage.
3. [MED] Documented that archived/unblocked are intentionally claimed (cursor
hygiene) but silent, and excluded from wake kinds.
4. [MED] Wake-injection failure now logs at WARNING with exc_info=True (the
cursor has already advanced, so a broken wake must not be a silent no-op).
Addresses @tonydwb's review on PR #54872 (12:05 UTC, 2026-06-29):
> the hardcoded Chinese text in the wake messages (lines 118-128 of
> the diff) should be replaced with English or internationalized.
> The rest of the codebase uses English for user-facing messages,
> and hardcoded Chinese will confuse non-Chinese users. Consider
> using a constants dict or the existing i18n infrastructure.
Used the existing i18n infrastructure (agent/i18n.py::t()) — the same
surface gateway/run.py and slash_commands.py already use for static
user-facing strings.
## Changes
- gateway/kanban_watchers.py: import `t` from agent.i18n; replace the
hardcoded Chinese strings in the synthetic wake-up message with
t("gateway.kanban.wake.*") lookups. Behavior unchanged for zh users
(zh catalog preserves the original Chinese phrasing).
- locales/en.yaml: new `gateway.kanban.wake.*` baseline keys (English):
completed / gave_up / crashed / timed_out / blocked / status_default
/ status_joiner / message (with {task_id} {status} {title}
{assignee} {board} placeholders).
- locales/zh.yaml: Chinese translation of the new keys, preserving the
exact wording the original code used (so existing zh users see no
visible change).
- locales/{zh-hant,ja,de,es,fr,tr,uk,af,ko,it,ga,pt,ru,hu}.yaml: added
the same key set with English fallback values. The i18n invariant
test (tests/agent/test_i18n.py::test_catalog_keys_match_english)
requires every catalog to carry the same key set as en.yaml; native
translations can land incrementally without breaking users (the
loader falls back to en.yaml per-key when a translation is missing,
but the key must still exist).
## Verification
- scripts/run_tests.sh tests/agent/test_i18n.py
tests/gateway/test_kanban_watchers_mixin.py
tests/gateway/test_kanban_notifier.py
tests/gateway/test_kanban_notifier_watcher_dispatch_gate.py
→ 60 passed, 0 failed (i18n catalog parity + placeholders parity +
existing kanban notifier behavior).
- Manual: with HERMES_LANGUAGE=en, t("gateway.kanban.wake.completed")
returns "completed"; with HERMES_LANGUAGE=zh, returns "已完成";
with HERMES_LANGUAGE=ja (translation pending), falls back to
"completed" per-key.
Three connected changes that fix kanban notifications in multiplex_profile
gateways and enable event-driven agent collaboration:
1. Session profile propagation
- Add HERMES_SESSION_PROFILE ContextVar (session_context.py)
- Gateway stamps source.profile at dispatch time (run.py)
- _maybe_auto_subscribe reads profile from ContextVar instead of
os.environ which is unset in the gateway main process (kanban_tools.py)
2. Notifier profile-aware routing (kanban_watchers.py)
- Adapter selection: prefer _profile_adapters[sub.notifier_profile]
so each profile's bot delivers its own task notifications
- Relax profile skip-filter: process cross-profile subscriptions when
the gateway has an adapter for the owning profile
- Extend TERMINAL_KINDS with status/archived/unblocked
3. Creator agent wakeup on terminal events (kanban_watchers.py)
- After delivering completed/blocked/gave_up/crashed/timed_out
notifications, inject a synthetic MessageEvent into the creator's
session via adapter.handle_message to trigger their agent loop
- SessionSource built from subscription metadata — no session_store
lookup needed
The gateway dispatcher captured kanban.auto_decompose ONCE at boot, so a user
who flipped it to false to STOP auto-decompose had no way to make that take
effect short of restarting the gateway. Reported (#49638): auto-decompose
created and launched tasks the user never intended (while they were still
typing the task description), and 'even Hermes Agent couldn't disable this
feature' — because the live config edit was silently ignored.
Auto-decompose is a safety toggle; turning it off must halt fan-out on the
next tick. The dispatcher now re-reads the flag (and auto_decompose_per_tick)
from config every tick via the extracted _resolve_auto_decompose_settings(),
which fails SAFE (disabled) on a config read error so a transient failure can
never re-enable a feature the user turned off.
Closes#49638.
ruff (unspecified-encoding) and the Windows-footgun checker both flag
open() in text mode without encoture=. Keep text mode (the Windows lock
path in _try_acquire_file_lock writes a str newline) and pass
encoding='utf-8'.
Two robustness gaps from community review (#44919):
1. Windows dead-path: replaced bespoke fcntl.flock with gateway.status
_try_acquire_file_lock / _release_file_lock — already cross-platform
(msvcrt on Windows, fcntl on POSIX). Added _release_singleton_lock
helper.
2. Lock fd never released: stored handle is now released explicitly in
both exit paths — CancelledError handler and normal while-loop exit.
Allows in-process stop/restart (tests, embedded use).
Also tightened docstrings — 'corrupt the SQLite DBs' is now specific
(wal_autocheckpoint=0 + concurrent manual WAL checkpoints can corrupt
index pages), matching the module's own concurrency claims.
The gateway's embedded dispatcher has no guard against more than one dispatcher
running concurrently. dispatch_in_gateway defaults to true, so a second gateway
for the same profile (a restart race where the old process is slow to exit) — or
any deployment that runs multiple profile gateways with the default — starts a
second dispatcher loop. As #41448 describes, concurrent dispatchers each run
release_stale_claims() against the same boards, double reclaim frequency, and
re-dispatch slow workers before they finish. In practice they also corrupt the
shared kanban SQLite DBs under concurrent write load.
Add _acquire_singleton_lock(): an exclusive, non-blocking fcntl.flock at the
machine-global kanban root (kanban_home()/kanban/.dispatcher.lock — the board is
shared across profiles by design, so this serialises every gateway, not just one
profile). The first gateway to start its dispatcher holds the lock for its
process lifetime; any other gateway finds it contended, logs, and skips
dispatching while still running for messaging. Falls back to config-only control
on non-POSIX or filesystems without flock.
This is more robust than a per-profile guard because the documented model is
"one dispatcher sweeps all boards" — the contention is across profiles, not just
within one. Closes#41448.
Test: lock is exclusive (held, then contended while held, then held again after
release).
gateway/run.py is the largest god file (20k LOC, GatewayRunner with 220
methods). This lifts the cohesive kanban-watcher cluster — _kanban_notifier_watcher,
_kanban_dispatcher_watcher, _kanban_advance/unsub/rewind, _deliver_kanban_artifacts
(~1,035 LOC, 6 methods) — into gateway/kanban_watchers.py as a mixin that
GatewayRunner inherits.
Mixin (not free functions) because the methods use only self state: inheriting
keeps every self._kanban_* call site working unchanged via the MRO, making this
a behavior-neutral move. The methods' lazy imports (_kb, _decomp, _load_config,
Platform) travel with them; the mixin needs only stdlib + a matching
logging.getLogger('gateway.run').
run.py 20187 -> 19157 LOC; GatewayRunner direct methods 220 -> 214.
Behavior-neutral: gateway test suite 6582 passed / 0 failed; start() still wires
both watchers via self._kanban_*; MRO resolves all 6 to the mixin. One test
(corrupt-board quarantine retry) keyed its time-travel mock on the caller's
filename being gateway/run.py — updated to also accept gateway/kanban_watchers.py.
Establishes the mixin-extraction pattern for further GatewayRunner decomposition
(the 2406-LOC _run_agent and 1164-LOC _handle_message remain, but their callback
closures need a context-object redesign — deferred).