Skip to content

✨ feat(kanban): inherit + reassign card origin across the spawn boundary - #55

Merged
cwest merged 2 commits into
cwest/integrationfrom
topic/origin-inherit-reassign
Jul 8, 2026
Merged

✨ feat(kanban): inherit + reassign card origin across the spawn boundary#55
cwest merged 2 commits into
cwest/integrationfrom
topic/origin-inherit-reassign

Conversation

@cwest

@cwest cwest commented Jul 8, 2026

Copy link
Copy Markdown
Owner

What & why

A card's origin — the delivery surface its transition wakes and completion notifications route to — is its kanban_notify_subs row, stamped at create time from the running process's own HERMES_SESSION_*. That's correct inside a live gateway session but wrong the moment a workstream crosses a spawn boundary into a detached context (dispatched worker, delegate_task subagent, background process, or a nested kanban_create from any of those): the session identity then names the detached run, not the human origin, so a wake for that work has nowhere real to land. There was also no way to re-point a card's origin when a workstream forks into a new thread.

The active-origin delivery + progressive fallback (owning-adapter dispatch, wake-precedence, thread→Home) is already merged; this PR builds the two structural properties that were still missing: origin inheritance and origin reassignability.

Design

Spec: docs/specs/kanban-origin-inheritance-reassign.md.

  • HERMES_KANBAN_ORIGIN — a standalone ContextVar + os.environ mirror, deliberately NOT a _VAR_MAP member. Membership would subject it to the per-message reset_session_vars strip and the _inject_session_context_env engaged-strip — both correct for session identity but wrong for an inheritable origin, which must survive the spawn boundary into a detached child that legitimately has an _UNSET session. It rides the already-copied os.environ and is overwritten only by an explicit set_kanban_origin (root capture or reassign), never implicitly.
  • Root capture at session bind (capture_root_origin_if_absent in _set_session_env) + a handler-entry reset (reset_kanban_origin, symmetric with reset_session_vars) so a live turn rebinds its own origin and a concurrent sibling's value can't leak in.
  • _maybe_auto_subscribe prefers the inherited origin over the running process's own session; falls back verbatim when none is inherited (live-session cards stay byte-identical → no regression).
  • Dispatcher seed: worker_origin_env reads the card's origin sub and _default_spawn seeds HERMES_KANBAN_ORIGIN into the worker env, so descendant cards re-inherit the human origin.
  • reassign_task_origin: atomically re-point a card's origin for a platform (delete same-platform subs + insert, cursor seeded to the latest event → no history replay; idempotent; optional descendant cascade). Exposed as the orchestrator-only kanban_reassign_origin tool, which also refreshes the caller's origin so subsequently-created child cards inherit the new surface.

Guard rails honoured

Reuses the existing owning-adapter / async-delivery path (no parallel delivery); preserves prompt caching + strict role alternation; keeps every HERMES_SESSION_* identity leak guard intact (origin rides a separate, intentionally-inherited channel); reassign is an explicit tool call (no passive behaviour change).

Tests

Behaviour-contract, not snapshots. C1–C4 (root capture / inheritance across the boundary / no-origin fallback / no identity leak) + D1–D3 (reassign replaces & seeds cursor / idempotent / fork inherits after reassign), the subprocess-bridge carry contract, and an E2E that drives the real chain end to end (worker_origin_env seed → real kanban_create in a detached worker session → real _maybe_auto_subscribe → real build_transition_payload) and asserts the wake targets the inherited (and reassigned) origin surface.

Blast-radius suites green: kanban_db (270), kanban_tools (104), session-context / leak-guard / notifier / transition / wake-routing / toolsets / ACP / MCP + the 7 new test files. Full tests/gateway run: 8683 passed; the 9 failures are pre-existing flakes/pollution in unrelated files (memory-monitor timer, telegram markdown-escaping) — verified failing on the clean base with none of the changed code in their paths.

cwest added 2 commits July 7, 2026 21:29
Autonomous-wake and terminal-state routing resolve a card's delivery
surface from its kanban_notify_subs row. That row is stamped at
card-create from the running process's own session identity, so a
workstream that crosses a spawn boundary into a detached context
(dispatched worker, delegated subagent, background process, nested
create) loses the human origin and its wakes route to an inert
surface. There is also no atomic way to re-point a card's origin to a
new thread when a workstream forks.

Spec an explicit, inheritable origin channel (HERMES_KANBAN_ORIGIN,
deliberately outside _VAR_MAP so it is not subject to the session
identity strip guards) that is captured at the root live session and
propagated to children, plus a reassign primitive/tool to move a
card's origin (optionally its descendants') to a new surface without
replaying history. The already-merged active-origin delivery and
progressive fallback are out of scope. Design gate — review before
TDD.
A card's origin — the delivery surface its transition wakes and completion
notifications route to — is its kanban_notify_subs row, stamped at create time
from the running process's own HERMES_SESSION_*. That is correct inside a live
gateway session but wrong the moment a workstream crosses a spawn boundary into
a detached context (dispatched worker, delegate_task subagent, background
process, or a nested create from any of those): the session identity then names
the detached run, not the human origin, so a wake for that work has nowhere real
to land. There was also no way to re-point a card's origin when a workstream
forks into a new thread.

Add an explicit, inheritable origin channel and a reassign primitive:

- HERMES_KANBAN_ORIGIN: a standalone ContextVar + os.environ mirror (NOT a
  _VAR_MAP member, so it is exempt from the per-message reset and the
  subprocess-env engaged-strip that session-identity vars get — it must SURVIVE
  the spawn boundary, the opposite requirement). set/get/capture helpers plus a
  root-capture at session bind and a handler-entry reset that mirrors the
  existing cross-session leak guards.
- _maybe_auto_subscribe prefers the inherited origin over the running process's
  own session, so a child card created in a detached worker subscribes the human
  origin. Falls back verbatim to prior behaviour when no origin is inherited, so
  live-session-created cards are byte-identical.
- The dispatcher seeds HERMES_KANBAN_ORIGIN into the worker env from the card's
  origin notify-sub (worker_origin_env), so descendant cards re-inherit it.
- reassign_task_origin: atomically re-point a card's origin for a platform
  (delete same-platform subs + insert, cursor-seeded to the latest event so no
  history replay; idempotent; optional descendant cascade). Exposed as the
  orchestrator-only kanban_reassign_origin tool, which also refreshes the
  caller's origin so subsequently-created child cards inherit the new surface.

Reuses the existing owning-adapter / async-delivery path (no parallel delivery),
preserves prompt caching + role alternation, and keeps all HERMES_SESSION_*
identity leak guards intact. Behaviour-contract tests C1–C4 + D1–D3 and an E2E
that drives the real seed → create → subscribe → wake-payload chain and asserts
the wake targets the inherited (and reassigned) origin surface.

@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 design holds up and matches the approved spec. Origin rides its own channel outside _VAR_MAP, so the 2026-06-21 identity-leak guards still pass unchanged, and the root-capture/reset ordering at the handler entry is correct. The inheritance path is wired end to end and I exercised the reassign tool through its real handler, not just the DB primitive: it re-points the sub and refreshes the context origin so later child cards pick up the new surface. Tests are green here too (34 new, plus the kanban_db and leak-guard suites).

One thing to fix before this lands, noted inline. reassign_task_origin promises "exactly one row for that platform," but the idempotency early-return can leave a stale sibling sub behind, which is the two-wakes-one-dark case this feature exists to close. Details on the line.

Comment thread hermes_cli/kanban_db.py
""",
(tid, platform, chat_id, thread_norm or ""),
).fetchone()
if existing is not None:

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 idempotency check returns as soon as it finds the exact target row, before the same-platform DELETE runs. When a card already carries two thread-bearing subs for one platform and you re-point to one of them, the other survives, so the card ends up with two origin subs for that platform. That contradicts the docstring invariant ("replaces the card's existing row(s) for that platform with exactly one new row") and reintroduces the exact dark-second-wake hazard the same-platform delete is there to prevent.

Reproduced against this head SHA:

add_notify_sub(discord, CHAN, tA)
add_notify_sub(discord, CHAN, tB)
reassign_task_origin(discord, CHAN, tA)
# -> subs still [(discord,CHAN,tA), (discord,CHAN,tB)]  # tB should be gone

The no-op intent (don't rewind a live cursor when re-pointing to the surface already in place) is right, but it should only short-circuit when that target is the card's sole same-platform sub. When other same-platform rows exist, fall through to the delete+insert so the invariant holds; preserve the existing row's cursor for the retained surface so re-pointing to the current thread still doesn't rewind it. A test with two same-platform subs re-pointed to one of them would lock this down (the current D2 test only has a single sub, so it passes through the early return without exercising the multi-sub case).

@cwest
cwest marked this pull request as ready for review July 8, 2026 04:23
@cwest
cwest merged commit 53cff68 into cwest/integration Jul 8, 2026
31 checks passed
@cwest
cwest deleted the topic/origin-inherit-reassign branch July 8, 2026 04:23
cwest added a commit that referenced this pull request Jul 26, 2026
…ary (#55)

* 📝 docs(kanban): spec origin inheritance + reassignability

Autonomous-wake and terminal-state routing resolve a card's delivery
surface from its kanban_notify_subs row. That row is stamped at
card-create from the running process's own session identity, so a
workstream that crosses a spawn boundary into a detached context
(dispatched worker, delegated subagent, background process, nested
create) loses the human origin and its wakes route to an inert
surface. There is also no atomic way to re-point a card's origin to a
new thread when a workstream forks.

Spec an explicit, inheritable origin channel (HERMES_KANBAN_ORIGIN,
deliberately outside _VAR_MAP so it is not subject to the session
identity strip guards) that is captured at the root live session and
propagated to children, plus a reassign primitive/tool to move a
card's origin (optionally its descendants') to a new surface without
replaying history. The already-merged active-origin delivery and
progressive fallback are out of scope. Design gate — review before
TDD.

* ✨ feat(kanban): inherit + reassign card origin across the spawn boundary

A card's origin — the delivery surface its transition wakes and completion
notifications route to — is its kanban_notify_subs row, stamped at create time
from the running process's own HERMES_SESSION_*. That is correct inside a live
gateway session but wrong the moment a workstream crosses a spawn boundary into
a detached context (dispatched worker, delegate_task subagent, background
process, or a nested create from any of those): the session identity then names
the detached run, not the human origin, so a wake for that work has nowhere real
to land. There was also no way to re-point a card's origin when a workstream
forks into a new thread.

Add an explicit, inheritable origin channel and a reassign primitive:

- HERMES_KANBAN_ORIGIN: a standalone ContextVar + os.environ mirror (NOT a
  _VAR_MAP member, so it is exempt from the per-message reset and the
  subprocess-env engaged-strip that session-identity vars get — it must SURVIVE
  the spawn boundary, the opposite requirement). set/get/capture helpers plus a
  root-capture at session bind and a handler-entry reset that mirrors the
  existing cross-session leak guards.
- _maybe_auto_subscribe prefers the inherited origin over the running process's
  own session, so a child card created in a detached worker subscribes the human
  origin. Falls back verbatim to prior behaviour when no origin is inherited, so
  live-session-created cards are byte-identical.
- The dispatcher seeds HERMES_KANBAN_ORIGIN into the worker env from the card's
  origin notify-sub (worker_origin_env), so descendant cards re-inherit it.
- reassign_task_origin: atomically re-point a card's origin for a platform
  (delete same-platform subs + insert, cursor-seeded to the latest event so no
  history replay; idempotent; optional descendant cascade). Exposed as the
  orchestrator-only kanban_reassign_origin tool, which also refreshes the
  caller's origin so subsequently-created child cards inherit the new surface.

Reuses the existing owning-adapter / async-delivery path (no parallel delivery),
preserves prompt caching + role alternation, and keeps all HERMES_SESSION_*
identity leak guards intact. Behaviour-contract tests C1–C4 + D1–D3 and an E2E
that drives the real seed → create → subscribe → wake-payload chain and asserts
the wake targets the inherited (and reassigned) origin surface.

(cherry picked from commit 53cff68)
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