fix(gateway): use connector-owned no-clobber guard for relay thread rename + trace logs (#75912)
Live staging (2026-08-01): relay semantic thread rename still declined silently despite both #74482 and #75581 deployed — thread kept its initial-words name, session title generated fine. Root cause is the no-clobber guard string mismatch (see paired gateway-gateway PR): the gateway can't reproduce the thread's initial name byte-for-byte, so the connector's only_if_current_name check always failed. - relay rename lane now passes prefer_connector_created=True instead of the fragile initial-name string; the connector resolves the guard from its own created-name memory. Native-marker lane keeps the legacy only_if_current_name string (source carries the real initial name). - rename_thread: prefer_connector_created param -> only_if_connector_created on the wire, precedence over the legacy string. - INFO logs at rename dispatch (thread/lane/new_title) and result (applied=bool): the whole failure hunt needed telemetry the gateway never emitted — this makes the outcome visible in fly logs. Tests: connector-guard wire shape + precedence over legacy string; the title-turn race test updated to assert the connector-owned guard. Relay suite 149 passed; ruff + footguns clean.
This commit is contained in:
parent
470cf66b03
commit
fed098bbf0
|
|
@ -2060,6 +2060,7 @@ class RelayAdapter(BasePlatformAdapter):
|
|||
name: str,
|
||||
*,
|
||||
only_if_current_name: Optional[str] = None,
|
||||
prefer_connector_created: bool = False,
|
||||
parent_chat_id: Optional[str] = None,
|
||||
) -> bool:
|
||||
"""Best-effort thread rename via the connector's `thread_rename` op.
|
||||
|
|
@ -2068,10 +2069,15 @@ class RelayAdapter(BasePlatformAdapter):
|
|||
called by the SAME semantic-rename lane (run.py
|
||||
_rename_discord_auto_thread_for_session_title), which fires only for
|
||||
sources carrying the connector-stamped auto-thread markers.
|
||||
``only_if_current_name`` crosses the wire; the CONNECTOR enforces the
|
||||
no-clobber guard (it owns the platform read), failing safe on
|
||||
platforms that can't read the current name. ``parent_chat_id`` is
|
||||
the containing chat where the caller knows it (Telegram needs it);
|
||||
|
||||
No-clobber guard: prefer ``prefer_connector_created=True``, which asks
|
||||
the CONNECTOR to enforce the guard from ITS OWN created-name memory
|
||||
(only_if_connector_created) — the gateway no longer has to reproduce
|
||||
the thread's initial name byte-for-byte, which drifted on any
|
||||
normalization difference and silently declined every relay rename.
|
||||
``only_if_current_name`` is the legacy string guard, kept for the
|
||||
native-marker lane and older connectors. ``parent_chat_id`` is the
|
||||
containing chat where the caller knows it (Telegram needs it);
|
||||
defaults to the thread id itself (Discord ignores chat_id).
|
||||
"""
|
||||
if self._transport is None or not self.descriptor.supports_op("thread_rename"):
|
||||
|
|
@ -2087,7 +2093,9 @@ class RelayAdapter(BasePlatformAdapter):
|
|||
"thread_name": cleaned[:100],
|
||||
"metadata": self._with_scope(chat_id, None),
|
||||
}
|
||||
if only_if_current_name is not None:
|
||||
if prefer_connector_created:
|
||||
action["only_if_connector_created"] = True
|
||||
elif only_if_current_name is not None:
|
||||
action["only_if_current_name"] = str(only_if_current_name)
|
||||
try:
|
||||
result = await self._transport.send_outbound(
|
||||
|
|
|
|||
|
|
@ -18962,18 +18962,36 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
if rename_thread is None:
|
||||
return
|
||||
target_thread_id = relay_info[0] if relay_info else str(source.thread_id)
|
||||
# Relay lane (relay_info present): ask the CONNECTOR to enforce the
|
||||
# no-clobber guard from its own created-name memory — the gateway
|
||||
# can't reliably reproduce the thread's initial name byte-for-byte
|
||||
# (normalization drift silently declined every rename before this).
|
||||
# Native-marker lane keeps the legacy string guard.
|
||||
use_connector_guard = relay_info is not None
|
||||
guard_name = (
|
||||
relay_info[1]
|
||||
if relay_info
|
||||
None
|
||||
if use_connector_guard
|
||||
else getattr(source, "auto_thread_initial_name", None)
|
||||
)
|
||||
thread_name = self._sanitize_discord_thread_title(title)
|
||||
logger.info(
|
||||
"discord auto-thread rename: thread=%s lane=%s new_title=%r",
|
||||
target_thread_id,
|
||||
"relay" if use_connector_guard else "native",
|
||||
thread_name,
|
||||
)
|
||||
try:
|
||||
await rename_thread(
|
||||
renamed = await rename_thread(
|
||||
target_thread_id,
|
||||
thread_name,
|
||||
prefer_connector_created=use_connector_guard,
|
||||
only_if_current_name=guard_name,
|
||||
)
|
||||
logger.info(
|
||||
"discord auto-thread rename result: thread=%s applied=%s",
|
||||
target_thread_id,
|
||||
bool(renamed),
|
||||
)
|
||||
except Exception:
|
||||
logger.debug("Failed to rename Discord auto-thread for generated session title", exc_info=True)
|
||||
|
||||
|
|
|
|||
|
|
@ -118,6 +118,38 @@ async def test_rename_thread_parent_chat_and_gating():
|
|||
assert gated_stub.sent == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rename_thread_prefers_connector_owned_guard():
|
||||
"""The relay lane sends only_if_connector_created (connector resolves the
|
||||
no-clobber guard from its own created-name memory) instead of the fragile
|
||||
cross-repo only_if_current_name string."""
|
||||
adapter, stub = _adapter()
|
||||
ok = await adapter.rename_thread(
|
||||
"th9", "Real Session Title", prefer_connector_created=True
|
||||
)
|
||||
assert ok is True
|
||||
action = stub.sent[-1]
|
||||
assert action["op"] == "thread_rename"
|
||||
assert action["only_if_connector_created"] is True
|
||||
# The fragile string guard is NOT sent when the connector owns the check.
|
||||
assert "only_if_current_name" not in action
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rename_thread_connector_guard_takes_precedence_over_string():
|
||||
"""prefer_connector_created wins even if a legacy string is also passed."""
|
||||
adapter, stub = _adapter()
|
||||
await adapter.rename_thread(
|
||||
"th9",
|
||||
"Title",
|
||||
prefer_connector_created=True,
|
||||
only_if_current_name="ignored initial words",
|
||||
)
|
||||
action = stub.sent[-1]
|
||||
assert action["only_if_connector_created"] is True
|
||||
assert "only_if_current_name" not in action
|
||||
|
||||
|
||||
# ── the relay semantic-rename lane (marker parity) ───────────────────────
|
||||
|
||||
|
||||
|
|
@ -291,8 +323,15 @@ async def test_title_rename_polls_feedback_that_arrives_late():
|
|||
adapter, stub_conn = _adapter()
|
||||
renames: list = []
|
||||
|
||||
async def rename_thread(thread_id, name, *, only_if_current_name=None, parent_chat_id=None):
|
||||
renames.append((thread_id, name, only_if_current_name))
|
||||
async def rename_thread(
|
||||
thread_id,
|
||||
name,
|
||||
*,
|
||||
only_if_current_name=None,
|
||||
prefer_connector_created=False,
|
||||
parent_chat_id=None,
|
||||
):
|
||||
renames.append((thread_id, name, prefer_connector_created))
|
||||
return True
|
||||
|
||||
adapter.rename_thread = rename_thread # type: ignore[method-assign]
|
||||
|
|
@ -308,7 +347,9 @@ async def test_title_rename_polls_feedback_that_arrives_late():
|
|||
src, "sess1", "Debugging the flux capacitor"
|
||||
)
|
||||
await task
|
||||
assert renames == [("th-9", "Debugging the flux capacitor", "Initial words")]
|
||||
# Relay lane uses the connector-owned guard (prefer_connector_created=True),
|
||||
# not the fragile cross-repo initial-name string.
|
||||
assert renames == [("th-9", "Debugging the flux capacitor", True)]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
Loading…
Reference in New Issue