✨ feat(kanban): route transition wakes back to the origin thread/session - #23
Conversation
cwest
left a comment
There was a problem hiding this comment.
The three-defect analysis holds up and the shape of the fix is right: one canonical delivery-profile resolver, an origin session_id stamped at create time, and a target resolver that never routes to a throwaway webhook session. I ran the new file (5 tests pass) and the existing transition-emit suite (11 pass), so the added origin fields keep the #22 event_type payload byte-stable. A few things to sort before this lands.
The main one is the F2 origin-key derivation, noted inline. The stamped session_id has to be the exact key the live inbound path produces for the same thread, because F3's later wake looks the card up by that key. Right now it isn't computed the same way, so it matches only on the current default config and silently misses under supported non-default settings — which puts the wake right back in the log, the thing this PR exists to stop.
Two smaller items: the wiring itself has no test (the new tests exercise the helper functions, not the slash-command stamp or the watcher gate), and there's a duplicate import shlex inside the create handler at slash_commands.py:313 — it's already imported at module level (line 25), so the local one can go.
One bookkeeping note: the PR body and the handoff say six new tests; the file has five. Not a problem on its own, just make the count match.
F3 consumption being deferred to the ops step is fine and clearly scoped. The concern is only that the key F2 writes and the key F3 will read must agree, and today they only agree by config coincidence.
Work moving through the kanban system did not proactively reach the human (in the thread the work was born in) or wake the orchestrator to proceed — a card transition either pinged the wrong place or died in the log, forcing manual "status / update / check it" polling. Three stacked defects, fixed together: F1 — subscription ownership. The notifier only delivers a sub whose `notifier_profile` matches the running notifier's profile, but subs were stamped with the CREATOR's profile. A sub made under a worker profile was silently dropped by the gateway notifier. Introduce a single canonical resolver `kanban_db.notifier_delivery_profile()` (config `kanban.notifier_profile` -> active profile -> "default") and use it at both the notifier's owner-profile gate and every subscribe site, so a sub is always owned by the gateway that will deliver it. New config key `kanban.notifier_profile` (default ""). F2 — origin session persistence. `tasks.session_id` existed and `create_task` accepted it, but the gateway /kanban create path never passed it, so every thread-born card had session_id=None. Add `--session-id` to `kanban create` and have the gateway stamp the origin session key (from the message source) onto cards created from a thread. The stamp derives its key through a shared `_origin_session_key()` helper that mirrors the live inbound path exactly — reads the same `group_sessions_per_user`/`thread_sessions_per_user` platform flags and injects no profile namespace — so the stamped key is byte-identical to the thread's live session key. Deriving it any other way (default per-user flags, or an injected profile) makes the key diverge under `thread_sessions_per_user: true` or a non-default notifier profile, and the F3 wake then targets a session that never existed. F3 — transition target. The transition wake fired into a contextless `webhook:kanban-transition:*` session and delivered to log. Add `resolve_transition_target()` (origin session+thread when known, else the default channel) and carry the origin (session + thread source) in the transition payload, so the woken orchestrator reports back to the origin thread. Origin fields are omitted when unknown, keeping the body byte-stable for the fallback case. Cache/alternation safe: the wake remains a normal inbound message on an idle session (the existing notifier chat-ping / notify_on_complete pattern); no mid-loop synthetic injection, no new core tool, no new env var. Tests: 9 tests in test_thread_origin_autonomy.py (profile resolver + ownership, session_id round-trip, target resolution, payload origin fields, and the origin-stamp-key == live-inbound-key wiring across the per-user flags and the agent:main namespace) RED->GREEN; PR-touched suites green (438 passed, 1 skipped) with no regression to the event_type classification fix.
c6a3a36 to
a5228cf
Compare
cwest
left a comment
There was a problem hiding this comment.
No changes needed. The rework holds.
The origin-key stamp now goes through a shared helper that derives the key the same way the live inbound path does: it reads group_sessions_per_user and thread_sessions_per_user off the platform config with the same defaults and passes no profile, so the namespace stays agent:main. I checked it against the inbound call and the build_session_key signature. The stamped key is now byte-identical to the thread's live session key on both the per-user axis and the namespace axis, so the wake targets a session that actually exists.
The duplicate local shlex import is gone; the module-level import at line 25 covers both call sites.
The four new tests lock the stamp derivation to the inbound derivation across thread_sessions_per_user, group_sessions_per_user, and the agent:main namespace, and the per-user cases assert the flag actually changes the inbound key so they can't pass vacuously. That equality test would have caught the original divergence.
Ran at this head: the origin-autonomy file is 9 passing tests (the body's count now matches), transition-emit stays 11 green so the event_type payload is byte-stable when the origin fields are unset, and the kanban-touched suites are 438 passed / 1 skipped with no new failures against the base. Merge signals are all green.
cwest
left a comment
There was a problem hiding this comment.
The rework at this head holds up. The origin-key stamp now goes through _origin_session_key(source, config_extra) in gateway/slash_commands.py, reading group_sessions_per_user/thread_sessions_per_user from the platform extra and injecting no profile, so the stamped key matches what the inbound path builds for the same source. The stamp site reads self.config.extra, matching base.py.
The four new equality tests build a thread SessionSource and assert the stamp key is byte-equal to the inbound key across thread_sessions_per_user: true, group_sessions_per_user: false, and the agent:main namespace, with a first assertion that each flag changes the inbound key so the equality can't pass vacuously. That closes the wiring gap. The duplicate local import shlex is gone; the body's test count matches the file at nine.
Ran the touched suites at this SHA: 380 passed, the nine in test_thread_origin_autonomy.py green on their own, ruff check clean on both changed files. Merge state is CLEAN, no conflicts, both review threads resolved. Nothing left to change here — ready to merge.
…(default-off) Optional, default-OFF bridge that lets a kanban lifecycle transition wake the orchestrator as an agent RUN (not merely a chat ping) by POSTing the transition to a loopback webhook route — mirroring how a GitHub pull_request event triggers a review run. New module gateway/kanban_transition_emit.py adds pure decision logic (should_emit_transition, build_transition_payload with a stable (board,task_id,kind,event_id) idempotency key) plus a fail-safe emit_transition coroutine that HMAC-signs and POSTs; it NEVER raises. The payload is classifiable by the webhook adapter, and transition wakes route back to the origin thread/session. Guarded by kanban.transition_emit.enabled (default OFF); when disabled the notifier path is byte-for-byte unchanged. No new core tool, no new model surface, no user-facing HERMES_* config var. upstream-pending: fork PR #21, #22, #23
…(default-off) Optional, default-OFF bridge that lets a kanban lifecycle transition wake the orchestrator as an agent RUN (not merely a chat ping) by POSTing the transition to a loopback webhook route — mirroring how a GitHub pull_request event triggers a review run. New module gateway/kanban_transition_emit.py adds pure decision logic (should_emit_transition, build_transition_payload with a stable (board,task_id,kind,event_id) idempotency key) plus a fail-safe emit_transition coroutine that HMAC-signs and POSTs; it NEVER raises. The payload is classifiable by the webhook adapter, and transition wakes route back to the origin thread/session. Guarded by kanban.transition_emit.enabled (default OFF); when disabled the notifier path is byte-for-byte unchanged. No new core tool, no new model surface, no user-facing HERMES_* config var. upstream-pending: fork PR #21, #22, #23 (cherry picked from commit d79d702)
Why
Work moving through the kanban system did not proactively reach Casey (in the thread the work was born in) or wake Hollis to proceed — a card transition either pinged the wrong place or died in the log, forcing manual "status / update / check it" polling. This closes the loop: work born in a thread reports back to that thread and wakes that session, autonomously.
Root cause — three stacked defects (all verified in code + live data)
F1 — subscription ownership (the silent-drop bug). The notifier only delivers a sub whose
notifier_profilematches the running notifier's profile (default), but subs were stamped with the creator's profile. Livenotify-listtoday: 61×salton, 4×hollis, 2×avram, only 5×default could ever deliver. This is why Lamport's PASS ping never reached Casey and why thread report-back was broken in general.→ Fix: one canonical resolver
kanban_db.notifier_delivery_profile()(configkanban.notifier_profile→ active profile →default), used at BOTH the notifier's owner-profile gate and every subscribe site, so a sub is always owned by the gateway that will deliver it. New config keykanban.notifier_profile(default"").F2 — origin session not persisted.
tasks.session_idexisted andcreate_taskaccepted it, but the gateway/kanban createpath never passed it (every thread-born card hadsession_id=None).→ Fix:
--session-idonkanban create; the gateway stamps the origin session key (from the message source) onto thread-created cards.F3 — transition wake hit a throwaway. The wake fired into a contextless
webhook:kanban-transition:*session, delivered tolog.→ Fix:
resolve_transition_target()(origin session+thread when known, else the default channel1515879019269197885); the transition payload now carries the origin (session + thread source), so the woken orchestrator reports back to the origin thread. Origin fields omitted when unknown → body stays byte-stable and the route falls back to the default channel.Design (locked with Casey)
One wire: card carries origin → any terminal transition delivers a message INTO the origin thread's session → Hollis wakes there with context, notices it, and either acts or waits for Casey (acceptance/merge gates and genuine forks stop for Casey). Non-thread work → default channel.
Safety
Cache/alternation safe — the wake is a normal inbound message on an idle session (existing notifier chat-ping /
notify_on_completepattern); no mid-loop synthetic injection, no new core tool, no newHERMES_*env var.Tests (TDD, RED→GREEN)
9 tests in
test_thread_origin_autonomy.py: profile resolver + ownership (worker-created sub still owned by delivering gateway), session_id round-trip, target resolution (origin vs fallback, never a webhook session), payload origin fields present-when-known/omitted-when-not, and — closing F1's wiring gap — the/kanban createorigin stamp key is byte-identical to the live inboundbuild_session_keyfor the same source acrossthread_sessions_per_user,group_sessions_per_user, and theagent:mainnamespace (no profile injected). PR-touched suites green (438 passed, 1 skipped), no regression to theevent_typeclassification fix (#22).Ops (post-merge, out of PR scope)
kanban.notifier_profile: defaultin the live config.default.kanban-transitionroute's handler to consumeorigin_*and fall back to1515879019269197885.