Skip to content

🐛 fix(gateway): re-read transition_emit config per notifier tick - #36

Merged
cwest merged 1 commit into
cwest/integrationfrom
topic/kanban-hotreload-transition-emit-cfg
Jul 2, 2026
Merged

🐛 fix(gateway): re-read transition_emit config per notifier tick#36
cwest merged 1 commit into
cwest/integrationfrom
topic/kanban-hotreload-transition-emit-cfg

Conversation

@cwest

@cwest cwest commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Why

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 on 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. This cost real debugging time —
a widened emit_kinds appeared set but the running process kept ignoring it.

What

  • Extract 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 config
    is unchanged. The helper falls back to safe defaults if a load fails, so a
    transient config error never crashes a notifier tick.
  • The internal secret env var (HERMES_KANBAN_TRANSITION_SECRET) is unchanged
    in behavior (still read only when enabled is True); only the config.yaml
    gate now hot-loads.

Done when

  • Changing kanban.transition_emit.emit_kinds takes effect on the next notifier
    tick with no gateway restart — proven by a regression test that 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.
  • No regression to the emit/ping decoupling or the notifier loop.

Tests

tests/gateway/test_kanban_notify_all_transitions.py
tests/gateway/test_kanban_transition_emit.py
tests/gateway/test_kanban_transition_emit_http.py

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.md row for the transition-emit bridge updated: the enabled/emit_kinds
config is now re-read per tick (no longer fully restart-gated).

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
cwest marked this pull request as ready for review July 2, 2026 01:35

@cwest cwest left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@cwest
cwest merged commit fac4252 into cwest/integration Jul 2, 2026
31 checks passed
@cwest
cwest deleted the topic/kanban-hotreload-transition-emit-cfg branch July 2, 2026 01:52
cwest added a commit that referenced this pull request Jul 26, 2026
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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant