Skip to content

Name a DM by the conversation the provider delivered into - #77

Closed
yuchanns wants to merge 3 commits into
mainfrom
f-20260909-dm-identity-from-provider
Closed

Name a DM by the conversation the provider delivered into#77
yuchanns wants to merge 3 commits into
mainfrom
f-20260909-dm-identity-from-provider

Conversation

@yuchanns

Copy link
Copy Markdown
Owner

A chat that does not exist yet can only be opened by the name it answers to, so a DM to a bot went out addressed by username and was stored under that name. The peer then spoke under its own chat id, which matched nothing, and one conversation became two: the outbound in one, the reply in the other, and a later send by handle rejected as ambiguous.

A send acknowledgement always names the conversation it landed in. Telegram now reads that id out of the acknowledgement and reports it on the delivery receipt, and a conversation whose stored name differs adopts it. Nothing distinguishes a bot from a person or a first message from a later one; the provider's own answer is the only authority. Storage gained a narrow rebind operation for this, because changing a conversation's provider identity stays forbidden everywhere else.

The migration names conversations that were stored before this. A peer's id is recoverable from what it said, since an inbound message carries both the handle it spoke under and the id it spoke from: where that peer already has a conversation of its own the two are merged, and where it does not the existing one is renamed. The merged cursor is held below the lowest message either side still had waiting, rather than taking the larger of the two, which would mark those messages delivered.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 10, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-10T04:22:37.950558Z 0db548f Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@yuchanns

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ec92bb52e6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

WHERE opened.target_kind = 'dm'
AND opened.target_handle IS NOT NULL
AND INSTR(opened.provider_thread_id, ':@' || opened.target_handle || ':') > 0
GROUP BY opened.agent_id, opened.id

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Pick the sender that actually minted the DM

When a Telegram username has appeared with more than one sender_id, this join returns multiple candidates but the GROUP BY selects an arbitrary identity. Since _resolve_or_mint uses the newest matching sender, an @kana session minted for the newer account can be migrated to an older account, causing its messages and reminders to be merged into the wrong peer thread and the original thread to be deleted. Select the applicable sender deterministically, such as by ranking matching messages by descending sequence before constructing peer_thread_id.

Useful? React with 👍 / 👎.

Comment on lines +447 to +450
await self._storage.rebind_channel_session(
channel_session.id,
provider_thread_id=delivered,
updated_at_ms=self._clock(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Merge an already-created acknowledged session atomically

If an inbound DM under the numeric provider ID is persisted while the outbound Telegram request is still awaiting its acknowledgement, record_inbound creates the numeric session first and this blind rebind then gives the handle-based session the same provider identity. The provider-identity index is non-unique, so the duplicate persists; subsequent find_channel_session calls reject the multiple rows and the conversation remains split. The rebind operation must atomically detect and merge/adopt an existing destination session rather than only updating this row.

Useful? React with 👍 / 👎.

Comment on lines +40 to +43
CASE
WHEN peer.target_handle IS NOT NULL THEN 'dm:@' || peer.target_handle
ELSE 'dm:' || peer.id
END AS peer_target

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve the retained session's canonical target

When two halves are merged and the retained peer has a handle, this stores moved messages under dm:@<handle> instead of the retained channel session's canonical dm:<session-id> target. read_message_history resolves the handle and then filters messages by target.canonical_target, so every moved outbound message is omitted from the merged conversation's history even though its thread_id was updated. Set peer_target to the retained session's canonical target; the handle should remain only a presentation alias.

Useful? React with 👍 / 👎.

Comment on lines +45 to +47
JOIN channel_sessions AS peer
ON peer.agent_id = renamed.agent_id
AND peer.provider_thread_id = renamed.peer_thread_id

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restrict the merge peer to the same channel

If another channel has the same opaque provider_thread_id, this join can select that channel's session because it matches only agent_id and provider identity, even though the repository defines provider identity using (agent_id, channel, provider_thread_id). The migration then moves messages into the other channel's thread while leaving their channel column unchanged and deletes the original DM session. Add a same-channel predicate when selecting peer.

Useful? React with 👍 / 👎.

Comment on lines 711 to +716
return ChannelDeliveryReceipt(
provider_message_id=confirmed_ids[0],
provider_receipt_ref=(
confirmed_ids[-1] if len(confirmed_ids) > 1 else None
),
provider_thread_id=provider_thread_id,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Replace the cached stream route with the acknowledged identity

After a first send by @username, _outbound_route has cached the handle-based identity under the local session ID, but the newly extracted numeric identity is only returned in the receipt and never replaces that cache entry. A later numeric inbound update is cached under its numeric-derived session ID, while orchestration continues emitting runtime events with the retained handle-derived local session ID, so typing actions keep targeting the old username. If that username changes or is reassigned, typing fails or is sent to the wrong chat; update _stream_routes for the request's session when the acknowledgement supplies the authoritative identity.

Useful? React with 👍 / 👎.

Comment thread tests/contrib/test_orchestration.py Outdated
try:
# This peer has only spoken in a group, so its DM does not exist yet and
# can only be opened by the name it answers to.
channel.delivered_thread_ids["test:dm:@kana"] = "test:dm:peer-1"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Verify provider-assigned DM IDs with a real Telegram e2e

This test makes the required provider behavior true by programming TestChannel.delivered_thread_ids, so it cannot detect the Telegram adapter failing to extract chat.id from an actual send acknowledgement, including the rich-message and document paths changed here. Since the behavior depends on an external provider response, cover it through the real Telegram e2e instead of treating a configurable fake receipt as verification.

AGENTS.md reference: AGENTS.md:L18-L18

Useful? React with 👍 / 👎.

@yuchanns
yuchanns force-pushed the f-20260909-dm-identity-from-provider branch from ec92bb5 to 05b040f Compare September 10, 2026 02:45
@yuchanns

Copy link
Copy Markdown
Owner Author

@codex review

1 similar comment
@yuchanns

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 05b040f8a9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +113 to +114
if request.delivery_handle is not None:
identity = replace(identity, chat_id=f"@{request.delivery_handle}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Stop overriding an acknowledged route with the handle

After a newly minted bot DM succeeds, the session still retains delivery_handle, and _adopt_delivered_thread_id returns without changing anything because the acknowledgement's numeric ID already equals the stored numeric ID. Consequently every later send enters this branch and replaces the stable numeric chat ID with @username; if the bot renames that username, the conversation starts failing, and if the name is reassigned, messages can be delivered to the new owner. Clear the one-time handle after acknowledgement or use it only until the numeric route has been confirmed.

Useful? React with 👍 / 👎.

Comment on lines +143 to +147
UPDATE channel_sessions SET provider_thread_id = (
SELECT peer_thread_id FROM bcn_renamed_dms
WHERE bcn_renamed_dms.opened_id = channel_sessions.id
)
WHERE id IN (SELECT opened_id FROM bcn_renamed_dms)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the handle when renaming an unopened legacy DM

For a pre-v27 handle session left behind by a failed or unconfirmed first send, this migration can infer the peer's numeric ID from a group message and rename the session even though the private chat was never opened. Because it does not also preserve the old username as delivery_handle, the next send uses the numeric route rather than the username required to open that chat and can no longer retry successfully. Record the delivery handle during this rename or restrict it to sessions whose private delivery was confirmed.

Useful? React with 👍 / 👎.

Comment on lines +137 to +141
def _delivery_handle(channel_session: ChannelSession) -> str | None:
"""The name that opens this chat, for as long as its id cannot."""

handle = channel_session.metadata.get("delivery_handle")
return handle if isinstance(handle, str) and handle else None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Inline the one-use delivery-handle lookup

_delivery_handle has exactly one call site and only unwraps one metadata value with a type/emptiness check, so it adds an extra indirection without defining a reusable abstraction. Inline this lookup where ChannelSendRequest is constructed, as the repository explicitly prohibits meaningless helper fragments used only once.

AGENTS.md reference: AGENTS.md:L2-L2

Useful? React with 👍 / 👎.

@yuchanns
yuchanns force-pushed the f-20260909-dm-identity-from-provider branch from 05b040f to 4dfbf46 Compare September 10, 2026 03:04
@yuchanns

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4dfbf464b0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +478 to +480
await self._storage.save_channel_session(
replace(
channel_session,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Refresh the session after rebinding before saving it

When a handle resolves to a different provider thread ID and no destination session exists, the rebind at line 468 changes the stored identity, but this subsequent save still passes the pre-rebind channel_session. Both the SQLite and in-memory repositories reject that as an attempt to change an immutable provider identity, so the command raises after Telegram has already delivered the message and the outbound remains unfinalized. Reload the rebound session, or combine the rebind and metadata removal into one storage operation.

Useful? React with 👍 / 👎.

Comment on lines +473 to +475
if (
delivery_result.state is not OutboundDeliveryState.SENT
or "delivery_handle" not in channel_session.metadata

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear the handle after an acknowledged partial delivery

For a multipart Telegram send where at least one part is confirmed before a later part fails, the PARTIAL result already contains the acknowledged numeric thread ID, but this SENT-only condition retains delivery_handle. The current code fixes confirmed sends, but every later message after this partial delivery is still routed through @username; if that username changes or is reassigned, delivery can fail or reach another peer even though the stable numeric route is known.

Useful? React with 👍 / 👎.

@yuchanns
yuchanns force-pushed the f-20260909-dm-identity-from-provider branch from 4dfbf46 to 008d542 Compare September 10, 2026 03:23
@yuchanns

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 008d542670

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +193 to +196
def delivered_thread_id(self, provider_thread_id: str) -> str:
"""Say which conversation a send landed in, the way a provider does."""

return self.delivered_thread_ids.get(provider_thread_id, provider_thread_id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Inline the one-use delivered-thread lookup

delivered_thread_id has exactly one call site and merely forwards to dict.get, so it adds an unnecessary indirection without defining a reusable abstraction. Inline this lookup when constructing ChannelDeliveryReceipt, as the repository explicitly prohibits meaningless helper fragments used only once.

AGENTS.md reference: AGENTS.md:L2-L2

Useful? React with 👍 / 👎.

Comment on lines +54 to +56
# the conversation the provider says this landed in, which is the only
# authority on a chat that was addressed by a name rather than by its id
provider_thread_id: str | None = None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Plan the acknowledged conversation identity change

The relevant plans/2026-09-05-dm-address-resolution.md still specifies @username as the Telegram bot DM identity and does not describe acknowledgement-derived provider identities, storage rebinding, or the v27 data migration introduced here. Because this is a material new identity flow rather than an implementation detail from that plan, it needs the required researched implementation plan and review sequence before landing.

AGENTS.md reference: AGENTS.md:L4-L6

Useful? React with 👍 / 👎.

Comment thread tests/contrib/test_sqlite_database.py Outdated
Comment on lines +1358 to +1362
assert [(row["id"], row["provider_thread_id"]) for row in sessions] == [
("channel-group", "telegram:1:-100:0"),
("channel-kana-chat", "telegram:1:7:0"),
("channel-mika-handle", "telegram:1:8:0"),
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Replace the exact migration snapshot assertion

This assertion requires the complete ordered set of session IDs and provider identities to match an exact snapshot, rather than checking only the migration invariants under test. That makes unrelated fixture additions or harmless ordering changes fail this test, and the repository explicitly prohibits exact assertions; assert the required retained, removed, and rebound properties independently instead.

AGENTS.md reference: AGENTS.md:L17-L17

Useful? React with 👍 / 👎.

@yuchanns
yuchanns force-pushed the f-20260909-dm-identity-from-provider branch 2 times, most recently from d455d89 to a8792e0 Compare September 10, 2026 03:48
@yuchanns

Copy link
Copy Markdown
Owner Author

@codex review

@yuchanns
yuchanns force-pushed the f-20260909-dm-identity-from-provider branch from a8792e0 to 3a613ba Compare September 10, 2026 04:06

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a8792e0dc9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +154 to +155
'$.delivery_handle',
target_handle

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve handles only for chats that were never opened

When an unmerged legacy handle session already contains a sent or partial outbound but has never received a DM reply, it still lands in bcn_renamed_dms; this unconditional JSON_SET reinstates delivery_handle even though a confirmed part already opened the numeric chat. If that username has since changed or been reassigned, the next outbound overrides the safe numeric ID with the stale handle and can fail or reach the wrong peer. The fresh issue is that the newly added assignment should be conditional on the session having no confirmed delivery.

Useful? React with 👍 / 👎.

Comment on lines +468 to +470
await self._storage.save_channel_session(
replace(
channel_session,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reload the session before clearing its delivery handle

If an inbound reply is persisted while the provider send is awaiting acknowledgement, record_inbound updates this same numeric session before execution resumes here. Saving the pre-send channel_session then overwrites the reply's last_inbound_at_ms and target presentation with stale values while merely trying to remove one metadata key, which can leave an obsolete handle bound to the conversation. Reload the row or perform a narrow atomic metadata update before saving.

Useful? React with 👍 / 👎.

Comment on lines +464 to +465
delivery_result.state
in (OutboundDeliveryState.SENT, OutboundDeliveryState.PARTIAL)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retire the handle after a confirmed prefix becomes unknown

For a multipart handle-routed send, an earlier part can be confirmed before a later part raises TelegramTransportError; _unknown retains the confirmed receipt entry but returns no typed provider_thread_id. Because this condition excludes UNKNOWN, the handle remains even though the confirmed part proves the numeric chat is open, so later sends continue through a renameable or reassigned username. Treat a confirmed receipt prefix as sufficient to clear the handle and adopt its acknowledged thread ID even when the overall delivery outcome is unknown.

Useful? React with 👍 / 👎.

Comment on lines +143 to +146
UPDATE channel_sessions SET
provider_thread_id = (
SELECT peer_thread_id FROM bcn_renamed_dms
WHERE bcn_renamed_dms.opened_id = channel_sessions.id

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reuse the renamed session by provider identity

For an unmerged legacy DM, this changes the provider identity in place but leaves its handle-derived local channel-session ID unchanged. If the peer later appears in a group under a new username before replying in the DM, _resolve_or_mint derives the numeric UUID, fails to find the migrated row by that ID, and then attempts to insert a second row with the same provider identity; save_channel_session rejects the duplicate, so dm:@newname cannot be sent. Fall back to locating the existing session by (channel, provider_thread_id) before creating it.

Useful? React with 👍 / 👎.

yuchanns and others added 3 commits September 10, 2026 00:13
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@yuchanns
yuchanns force-pushed the f-20260909-dm-identity-from-provider branch from 3a613ba to 0db548f Compare September 10, 2026 04:14
@yuchanns

Copy link
Copy Markdown
Owner Author

@codex review

@yuchanns yuchanns closed this Sep 10, 2026
@yuchanns
yuchanns deleted the f-20260909-dm-identity-from-provider branch September 10, 2026 04:20
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: 0db548fc35

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

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