-
Notifications
You must be signed in to change notification settings - Fork 0
Reach one peer's DM by every name it answers to #76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from .model import Migration | ||
|
|
||
| # A DM the agent opened by handle and the same DM the peer speaks in are one | ||
| # conversation stored twice, because each side named it its own way. They are | ||
| # recognisable as a pair: one row is keyed by the very handle both answer to and | ||
| # the other is not. Two different peers who share a display name are not, since | ||
| # neither of their rows is keyed by that name. | ||
| _PAIRS = """ | ||
| SELECT | ||
| loser.agent_id AS agent_id, | ||
| loser.id AS loser_id, | ||
| loser_thread.id AS loser_thread_id, | ||
| winner.id AS winner_id, | ||
| winner_thread.id AS winner_thread_id, | ||
| CASE | ||
| WHEN winner.target_handle IS NOT NULL THEN 'dm:@' || winner.target_handle | ||
| ELSE 'dm:' || winner.id | ||
| END AS winner_target | ||
| FROM channel_sessions AS loser | ||
| JOIN threads AS loser_thread | ||
| ON loser_thread.agent_id = loser.agent_id | ||
| AND loser_thread.channel_session_id = loser.id | ||
| JOIN channel_sessions AS winner | ||
| ON winner.agent_id = loser.agent_id | ||
| AND winner.channel = loser.channel | ||
| AND winner.target_kind = 'dm' | ||
| AND winner.target_handle_key = loser.target_handle_key | ||
| AND winner.id <> loser.id | ||
| AND LOWER(winner.provider_thread_id) | ||
| NOT LIKE '%:@' || winner.target_handle_key || ':%' | ||
| JOIN threads AS winner_thread | ||
| ON winner_thread.agent_id = winner.agent_id | ||
| AND winner_thread.channel_session_id = winner.id | ||
| WHERE loser.target_kind = 'dm' | ||
| AND loser.target_handle_key IS NOT NULL | ||
| AND LOWER(loser.provider_thread_id) | ||
| LIKE '%:@' || loser.target_handle_key || ':%' | ||
| AND ( | ||
| SELECT COUNT(*) FROM channel_sessions AS peer | ||
| WHERE peer.agent_id = loser.agent_id | ||
| AND peer.channel = loser.channel | ||
| AND peer.target_kind = 'dm' | ||
| AND peer.target_handle_key = loser.target_handle_key | ||
| ) = 2 | ||
| """ | ||
|
|
||
| SPLIT_DM_MERGE_MIGRATION = Migration( | ||
| version=27, | ||
| name="merge_split_dm_conversations", | ||
| statements=( | ||
| # the rows that identify a pair are the rows this merge deletes, so the | ||
| # set has to be settled before the first of them goes | ||
| f""" | ||
| CREATE TEMPORARY TABLE bcn_split_dm_pairs AS {_PAIRS} | ||
| """, | ||
| # the surviving cursor cannot fall behind messages it is about to own | ||
| """ | ||
| WITH pairs AS (SELECT * FROM bcn_split_dm_pairs) | ||
| UPDATE consumer_cursors SET delivered_through_seq = MAX( | ||
| delivered_through_seq, | ||
| COALESCE(( | ||
| SELECT losing.delivered_through_seq | ||
| FROM consumer_cursors AS losing | ||
| JOIN pairs ON pairs.loser_thread_id = losing.thread_id | ||
| WHERE pairs.winner_thread_id = consumer_cursors.thread_id | ||
| ), 0) | ||
|
Comment on lines
+61
to
+68
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the losing thread has a higher global Useful? React with 👍 / 👎. |
||
| ) | ||
| WHERE thread_id IN (SELECT winner_thread_id FROM pairs) | ||
| """, | ||
| """ | ||
| WITH pairs AS (SELECT * FROM bcn_split_dm_pairs) | ||
| DELETE FROM consumer_cursors | ||
| WHERE thread_id IN (SELECT loser_thread_id FROM pairs) | ||
| """, | ||
| """ | ||
| WITH pairs AS (SELECT * FROM bcn_split_dm_pairs) | ||
| UPDATE reminders SET owner_thread_id = ( | ||
| SELECT winner_thread_id FROM pairs | ||
| WHERE pairs.loser_thread_id = reminders.owner_thread_id | ||
| ) | ||
| WHERE owner_thread_id IN (SELECT loser_thread_id FROM pairs) | ||
| """, | ||
| # provider_thread_id stays as it was: it records how this very message | ||
| # was addressed, and rewriting it would claim it arrived another way | ||
| """ | ||
| WITH pairs AS (SELECT * FROM bcn_split_dm_pairs) | ||
| UPDATE messages SET | ||
| thread_id = ( | ||
| SELECT winner_thread_id FROM pairs | ||
| WHERE pairs.loser_id = messages.channel_session_id | ||
| ), | ||
| target = ( | ||
| SELECT winner_target FROM pairs | ||
| WHERE pairs.loser_id = messages.channel_session_id | ||
| ), | ||
| channel_session_id = ( | ||
| SELECT winner_id FROM pairs | ||
| WHERE pairs.loser_id = messages.channel_session_id | ||
| ) | ||
| WHERE channel_session_id IN (SELECT loser_id FROM pairs) | ||
| """, | ||
| """ | ||
| WITH pairs AS (SELECT * FROM bcn_split_dm_pairs) | ||
| DELETE FROM threads WHERE id IN (SELECT loser_thread_id FROM pairs) | ||
| """, | ||
| """ | ||
| WITH pairs AS (SELECT * FROM bcn_split_dm_pairs) | ||
| DELETE FROM channel_sessions WHERE id IN (SELECT loser_id FROM pairs) | ||
| """, | ||
| """ | ||
| DROP TABLE bcn_split_dm_pairs | ||
| """, | ||
| ), | ||
| ) | ||
|
|
||
| __all__ = ["SPLIT_DM_MERGE_MIGRATION"] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -266,14 +266,17 @@ async def _resolve_or_mint( | |
| if address is None: | ||
| raise | ||
| now = self._clock() | ||
| stored_session = await self._storage.get_channel_session( | ||
| address.channel_session_id | ||
| # the peer may already have written first under another of its | ||
| # identities, and that conversation is this one | ||
| stored_session = await self._storage.find_channel_session( | ||
| channel=known.channel, | ||
| provider_thread_ids=address.provider_thread_ids, | ||
| ) | ||
| if stored_session is None: | ||
| stored_session = ChannelSession( | ||
| id=address.channel_session_id, | ||
| channel=known.channel, | ||
| provider_thread_id=address.provider_thread_id, | ||
| provider_thread_id=address.provider_thread_ids[0], | ||
|
Comment on lines
275
to
+279
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For a Telegram bot, Useful? React with 👍 / 👎. |
||
| created_at_ms=now, | ||
| updated_at_ms=now, | ||
| target_kind=ChannelTargetKind.DM, | ||
|
|
@@ -289,11 +292,11 @@ async def _resolve_or_mint( | |
| target_handle_key=handle.casefold(), | ||
| ) | ||
| ) | ||
| stored_thread = await self._storage.get_thread(address.thread_id) | ||
| stored_thread = await self._storage.find_thread(stored_session.id) | ||
| if stored_thread is None: | ||
| stored_thread = ConversationRow( | ||
| id=address.thread_id, | ||
| channel_session_id=address.channel_session_id, | ||
| channel_session_id=stored_session.id, | ||
| workspace_id=self._actors.agent_id, | ||
| created_at_ms=now, | ||
| updated_at_ms=now, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,7 @@ | |
| from ..lifecycle import IAsyncLifecycle, TimeoutBudget | ||
| from ..models import ( | ||
| ChannelSession, | ||
| ChannelTargetKind, | ||
| Message, | ||
| RuntimeAttempt, | ||
| RuntimeEventState, | ||
|
|
@@ -1034,12 +1035,46 @@ def _runtime_worker_done( | |
| ) | ||
| self._start_runtime_worker(actor, queue) | ||
|
|
||
| async def _settle_provider_thread_id(self, message: Message) -> Message: | ||
| """Name this conversation the way it is already stored. | ||
|
|
||
| A channel that can address one peer several ways may hand this message | ||
| the name that peer speaks under while the conversation was opened under | ||
| another. Only the stored row settles which one it answers to, and from | ||
| here on there is one name again. | ||
| """ | ||
|
|
||
| if ( | ||
| message.target_kind is not ChannelTargetKind.DM | ||
| or message.channel is None | ||
| or message.provider_thread_id is None | ||
| or message.sender is None | ||
| ): | ||
| return message | ||
| address = self._channel.dm_address( | ||
| message.sender, sender_kind=message.sender_kind | ||
| ) | ||
| if address is None: | ||
| return message | ||
| candidates = tuple( | ||
| dict.fromkeys((message.provider_thread_id, *address.provider_thread_ids)) | ||
| ) | ||
| if len(candidates) == 1: | ||
| return message | ||
| stored = await self._storage.find_channel_session( | ||
| channel=message.channel, | ||
| provider_thread_ids=candidates, | ||
|
Comment on lines
+1059
to
+1066
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the database contains both the exact numeric session for this inbound message and another stale session under one of the supplied aliases, querying all candidates together makes Useful? React with 👍 / 👎. |
||
| ) | ||
| if stored is None or stored.provider_thread_id == message.provider_thread_id: | ||
| return message | ||
| return replace(message, provider_thread_id=stored.provider_thread_id) | ||
|
Comment on lines
+1068
to
+1070
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a numeric Telegram inbound is settled onto a conversation opened under Useful? React with 👍 / 👎. |
||
|
|
||
| async def _record_inbound( | ||
| self, | ||
| message: Message, | ||
| ) -> tuple[_DurableTurnContext | None, Message, bool]: | ||
| recorded = await self._storage.record_inbound( | ||
| message, | ||
| await self._settle_provider_thread_id(message), | ||
| now_ms=self._clock(), | ||
| ) | ||
| message = recorded.message | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Matching two rows by
target_handle_keydoes not prove that they belong to the same Telegram peer because usernames can be renamed and reassigned. If a handle-keyed conversation belongs to the former owner and a numeric conversation records the new owner under that same handle, these predicates classify them as a pair; the later statements then combine their private message histories and reminders and delete one conversation. Require evidence involving the stable numeric peer identity before performing this destructive merge.Useful? React with 👍 / 👎.