Skip to content

fix(relay): rename sibling auto-threads via prospective_thread_id (not per-chat cache) - #77052

Merged
benbarclay merged 1 commit into
mainfrom
fix/relay-rename-prospective-thread-keying
Aug 2, 2026
Merged

fix(relay): rename sibling auto-threads via prospective_thread_id (not per-chat cache)#77052
benbarclay merged 1 commit into
mainfrom
fix/relay-rename-prospective-thread-keying

Conversation

@benbarclay

Copy link
Copy Markdown
Collaborator

Problem

The last of the four original Discord auto-thread symptoms: message B's thread doesn't get its semantic rename. In live staging testing, message A's thread renamed correctly (e.g. "A Hundred Word Sword Story") but a second message B in the same channel created a thread that stayed stuck at its raw first-words name.

Root cause

The gateway's semantic thread-rename lane resolves the target thread via _relay_auto_thread_info(source), which read a single-slot-per-parent-chat cacheadapter._auto_thread_by_chat[chat_id], populated from the connector's SendResult feedback (thread_id + auto_thread_name).

When two auto-threads spawn from the same parent channel:

  1. A's reply → cache[chat] = (threadA, "…") → A's title turn reads it → A renames ✅
  2. B's reply → overwrites cache[chat] = (threadB, "…"), and B's title turn read races the write

So only the first thread in any channel reliably renamed. This is the same per-channel-vs-per-thread bug class fixed for session keying in the prior change — but one layer down, in the rename lane.

Live evidence (staging, 2026-08-02): both threads created (bot-owned, created-name store entries present), A renamed, B's discord auto-thread rename: thread=<B> line never fired.

Fix

The connector now stamps prospective_thread_id on the inbound (the anchor message id = the id of the thread it will auto-create), shipped for per-thread session keying. Reuse it in the rename lane: it's deterministic and per-message, so it names the exact thread even when several auto-threads share one channel.

_relay_auto_thread_info now returns (prospective_thread_id, "") directly and never consults the collision-prone per-chat cache. The empty initial-name marker defers no-clobber enforcement to the connector's own created-name guard (prefer_connector_created=True), which is keyed per-thread and already in place. The send-result cache path remains as a fallback for older connectors that don't stamp the field — no behavior change there.

Scope

Single gateway-side change in gateway/run.py::_relay_auto_thread_info. No schema, no connector change, no cache-breaking. Backward compatible.

Tests

Two new cases in tests/gateway/relay/test_relay_threads.py:

  • test_relay_auto_thread_info_prefers_prospective_thread_id — prospective id wins over a poisoned cache entry (proves the cache isn't read)
  • test_sibling_threads_in_one_channel_each_rename_to_own_thread — two sibling threads in one channel each rename to their own thread id

Full gateway session + relay suites green: 211 passed.

scripts/run_tests.sh tests/gateway/test_session.py tests/gateway/relay/   → 211 passed
ruff check gateway/run.py tests/gateway/relay/test_relay_threads.py       → All checks passed!

Live verification

Deployed to staging (single current-code instance) and confirmed the full chain earlier: connector stamps → gateway logs has_prospective=True prospective=<thread_id>. This PR closes the rename-lane gap that remained after session keying landed.

…chat cache

The Discord semantic thread-rename lane resolved the target thread from
`_relay_auto_thread_info`, which read a single-slot-per-parent-chat cache
(`adapter._auto_thread_by_chat[chat_id]`, populated from connector
SendResult feedback). When two auto-threads spawned from the SAME parent
channel, the second send overwrote the first's slot and the title turn's
read raced the write — so only the FIRST thread in a channel ever got its
semantic rename. Staging repro 2026-08-02: message A's thread renamed to
"A Hundred Word Sword Story", sibling message B's thread stayed stuck at
the raw first-words name.

The connector now stamps `prospective_thread_id` on the inbound (the anchor
message id, which is the id of the thread it will auto-create) — shipped for
per-thread session keying. Reuse it here: it is deterministic and
per-message, so it names the EXACT thread even when several auto-threads
share one channel. `_relay_auto_thread_info` returns it directly (with an
empty initial-name marker) and never consults the collision-prone per-chat
cache; the connector's own created-name guard (`prefer_connector_created`)
still enforces no-clobber, so no initial name is needed gateway-side. The
send-result cache path stays as a fallback for older connectors that don't
stamp the field.

Tests: two new cases in test_relay_threads.py — prospective id wins over a
poisoned cache entry, and two sibling threads in one channel each rename to
their own thread id. Full gateway session + relay suites green (211 passed).
@benbarclay
benbarclay enabled auto-merge (squash) August 2, 2026 19:33
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on d5b66a6

all good!

@benbarclay
benbarclay merged commit d0b87da into main Aug 2, 2026
35 checks passed
@benbarclay
benbarclay deleted the fix/relay-rename-prospective-thread-keying branch August 2, 2026 19:35
@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have comp/gateway Gateway runner, session dispatch, delivery platform/discord Discord bot adapter sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages labels Aug 2, 2026
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
…chat cache (NousResearch#77052)

The Discord semantic thread-rename lane resolved the target thread from
`_relay_auto_thread_info`, which read a single-slot-per-parent-chat cache
(`adapter._auto_thread_by_chat[chat_id]`, populated from connector
SendResult feedback). When two auto-threads spawned from the SAME parent
channel, the second send overwrote the first's slot and the title turn's
read raced the write — so only the FIRST thread in a channel ever got its
semantic rename. Staging repro 2026-08-02: message A's thread renamed to
"A Hundred Word Sword Story", sibling message B's thread stayed stuck at
the raw first-words name.

The connector now stamps `prospective_thread_id` on the inbound (the anchor
message id, which is the id of the thread it will auto-create) — shipped for
per-thread session keying. Reuse it here: it is deterministic and
per-message, so it names the EXACT thread even when several auto-threads
share one channel. `_relay_auto_thread_info` returns it directly (with an
empty initial-name marker) and never consults the collision-prone per-chat
cache; the connector's own created-name guard (`prefer_connector_created`)
still enforces no-clobber, so no initial name is needed gateway-side. The
send-result cache path stays as a fallback for older connectors that don't
stamp the field.

Tests: two new cases in test_relay_threads.py — prospective id wins over a
poisoned cache entry, and two sibling threads in one channel each rename to
their own thread id. Full gateway session + relay suites green (211 passed).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have platform/discord Discord bot adapter sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants