🐛 fix(gateway): re-read transition_emit config per notifier tick - #36
Conversation
The kanban notifier read transition_emit_cfg (enabled / emit_kinds) and the notify_fallback target ONCE before its `while self._running` poll loop, then used the cached values inside every tick. Any change to kanban.transition_emit.emit_kinds or .enabled was therefore silently restart-gated: editing the live config had no effect until a gateway restart, with nothing signalling the setting was stale. Move the derivation into a small _resolve_transition_cfg() helper backed by a FRESH load_config() and call it at the top of each tick, mirroring how the tick interval is re-resolved per call. The _collect closure captures these names by reference, so rebinding them per tick makes the change visible to that tick's claim/emit gates. load_config() is a cheap in-memory read; behavior is unchanged when the config is unchanged. The helper falls back to safe defaults if a load fails so a transient config error never crashes the tick. A regression test flips emit_kinds mid-run across two ticks (via a fresh config object, as an on-disk edit produces) and asserts the widened gate is honored on the next tick with no restart.
cwest
left a comment
There was a problem hiding this comment.
The transition-emit and notify_fallback config now derives from a fresh load_config() at the top of each tick instead of once before the loop, so an edit to kanban.transition_emit.emit_kinds/.enabled takes effect on the next tick with no restart. That is the right fix for the reported bug.
The two mechanics this depends on both hold:
- load_config() re-reads from disk when config.yaml changes. Its cache is keyed on the file's (mtime_ns, size), so an on-disk edit invalidates the entry and forces a re-merge; an unchanged file returns a cheap deepcopy of the cached value. So the per-tick call hot-loads real edits and stays cheap when nothing changed.
- The _collect closure and the emit call site read transition_emit_cfg / transition_emit_secret / fallback_chat_id / fallback_platform as free variables, and _collect is rebuilt inside the loop after the per-tick rebind, so each tick sees that tick's values. No stale reference to the old pre-loop derivation remains.
The fail-closed default in _resolve_transition_cfg (empty cfg, secret None on a load error) disables the wake for that tick rather than crashing it, which is the safe behavior.
Verified independently in a throwaway clone at the head SHA: reverting kanban_watchers.py to its parent makes the new test fail with the restart-gated symptom, and it passes on the fix. The transition-emit / notifier / wake / origin-routing surface is green (232 passed, 1 skipped across the kanban/notifier/transition tests; the emit/ping decoupling tests included). No changes needed.
The kanban notifier read transition_emit_cfg (enabled / emit_kinds) and the notify_fallback target ONCE before its `while self._running` poll loop, then used the cached values inside every tick. Any change to kanban.transition_emit.emit_kinds or .enabled was therefore silently restart-gated: editing the live config had no effect until a gateway restart, with nothing signalling the setting was stale. Move the derivation into a small _resolve_transition_cfg() helper backed by a FRESH load_config() and call it at the top of each tick, mirroring how the tick interval is re-resolved per call. The _collect closure captures these names by reference, so rebinding them per tick makes the change visible to that tick's claim/emit gates. load_config() is a cheap in-memory read; behavior is unchanged when the config is unchanged. The helper falls back to safe defaults if a load fails so a transient config error never crashes the tick. A regression test flips emit_kinds mid-run across two ticks (via a fresh config object, as an on-disk edit produces) and asserts the widened gate is honored on the next tick with no restart. (cherry picked from commit fac4252)
Why
The kanban notifier read
transition_emit_cfg(enabled/emit_kinds) andthe
notify_fallbacktarget once, before itswhile self._runningpollloop, then used the cached values on every tick. Any change to
kanban.transition_emit.emit_kindsor.enabledwas therefore silentlyrestart-gated: editing the live config had no effect until a gateway restart,
with nothing signalling the setting was stale. This cost real debugging time —
a widened
emit_kindsappeared set but the running process kept ignoring it.What
_resolve_transition_cfg()helper backedby a fresh
load_config(), and call it at the top of each tick — mirroringhow the tick interval is re-resolved per call. The
_collectclosure capturesthese names by reference, so rebinding them per tick makes the change visible
to that tick's claim/emit gates.
load_config()is a cheap in-memory read; behavior is unchanged when configis unchanged. The helper falls back to safe defaults if a load fails, so a
transient config error never crashes a notifier tick.
HERMES_KANBAN_TRANSITION_SECRET) is unchangedin behavior (still read only when
enabled is True); only theconfig.yamlgate now hot-loads.
Done when
kanban.transition_emit.emit_kindstakes effect on the next notifiertick with no gateway restart — proven by a regression test that flips
emit_kindsmid-run (across two ticks, via a fresh config object as an on-diskedit produces) and asserts the widened gate is honored on the next tick.
Tests
Full blast-radius suite (every test touching the notifier / transition-emit /
kanban notify): 253 passed, 1 skipped. The new mid-run hot-reload test fails
on the pre-fix code (cached config) and passes with the fix.
PATCHES.mdrow for the transition-emit bridge updated: theenabled/emit_kindsconfig is now re-read per tick (no longer fully restart-gated).