Skip to content

✨ feat(kanban): route transition wakes back to the origin thread/session - #23

Merged
cwest merged 1 commit into
cwest/integrationfrom
topic/thread-origin-autonomy
Jul 1, 2026
Merged

✨ feat(kanban): route transition wakes back to the origin thread/session#23
cwest merged 1 commit into
cwest/integrationfrom
topic/thread-origin-autonomy

Conversation

@cwest

@cwest cwest commented Jul 1, 2026

Copy link
Copy Markdown
Owner

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_profile matches the running notifier's profile (default), but subs were stamped with the creator's profile. Live notify-list today: 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() (config kanban.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 key kanban.notifier_profile (default "").

F2 — origin session not persisted. tasks.session_id existed and create_task accepted it, but the gateway /kanban create path never passed it (every thread-born card had session_id=None).
Fix: --session-id on kanban 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 to log.
Fix: resolve_transition_target() (origin session+thread when known, else the default channel 1515879019269197885); 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_complete pattern); no mid-loop synthetic injection, no new core tool, no new HERMES_* 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 create origin stamp key is byte-identical to the live inbound build_session_key for the same source across thread_sessions_per_user, group_sessions_per_user, and the agent:main namespace (no profile injected). PR-touched suites green (438 passed, 1 skipped), no regression to the event_type classification fix (#22).

Ops (post-merge, out of PR scope)

  • Set kanban.notifier_profile: default in the live config.
  • Re-stamp existing mis-owned live subs → default.
  • Point the kanban-transition route's handler to consume origin_* and fall back to 1515879019269197885.
  • Restart to load merged code (restart-gated).

@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 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.

Comment thread gateway/slash_commands.py
Comment thread tests/gateway/test_thread_origin_autonomy.py
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.
@cwest
cwest force-pushed the topic/thread-origin-autonomy branch from c6a3a36 to a5228cf Compare July 1, 2026 03:48

@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.

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
cwest marked this pull request as ready for review July 1, 2026 04:02

@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 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.

@cwest
cwest merged this pull request into cwest/integration Jul 1, 2026
8 checks passed
@cwest
cwest deleted the topic/thread-origin-autonomy branch July 1, 2026 04:11
cwest added a commit that referenced this pull request Jul 1, 2026
…(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
cwest added a commit that referenced this pull request Jul 26, 2026
…(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)
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