Skip to content

feat(core): Thread.reply, Thread.mark_as_read, post_ephemeral options (#200) - #294

Merged
patrick-chinchill merged 3 commits into
mainfrom
sync/4.41-c6
Oct 1, 2026
Merged

patrick-chinchill merged 3 commits into
mainfrom
sync/4.41-c6

Conversation

@patrick-chinchill

@patrick-chinchill patrick-chinchill commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Adds the core thread APIs from chat@4.35–4.41: Thread.reply(target, message), Thread.mark_as_read(message=None), and post_ephemeral forwarding the caller's PostEphemeralOptions (an adapter may now return None). This PR is core-only. Adapter implementations come in #228 (Telegram replies), #239 (WhatsApp/Messenger reply and read receipts), #219 (Teams targeted ephemeral) and #241 (Gmail).

Upstream commits mapped

Upstream Python
83ede7ea feat(chat): add message reply support (#819, chat@4.38.0) ThreadImpl.reply, _find_known_message, _create_sent_message(reply_to=), edit keeps thread_id + reply_to, reply documented as an optional hook on BaseAdapter (no default), Thread Protocol reply
18d4a230 feat(chat): add mark as read support (#820, chat@4.38.0) ThreadImpl.mark_as_read, mark_as_read documented as an optional hook on BaseAdapter (no default), Thread Protocol mark_as_read, mock adapter mark_as_read
160140e3 feat(teams): targeted ephemeral (#737, chat@4.35.0) core part is docs only (BaseAdapter.post_ephemeral docstring); Teams half is #219
bfee00af feat(gmail) (#939, chat@4.41.0), core slice post_ephemeral(..., *, options=None) -> EphemeralMessage | None; Thread/Channel.post_ephemeral forward options; Slack and Google Chat accept and ignore it

Behavior

  • reply(): in upstream order, checks for the hook, resolves the target (current message, then recent_messages; never fetched), rejects a cross-thread Message, buffers a stream through from_full_stream into PostableMarkdown (" " when there is no text), processes callback URLs (thread scope), calls adapter.reply, builds a SentMessage with reply_to, then appends to history.
  • mark_as_read(): errors in upstream order (no hook → MESSAGE_REQUIRED → THREAD_MISMATCH). None means the current message, checked with is None. An explicit "" is rejected as MESSAGE_REQUIRED, as upstream ("" ?? current then !target).
  • SentMessage.edit() now keeps the resolved thread_id. Before, a message posted under a thread_id_override reverted to self._id on edit (handed off from [4.41/C3] Conversation context + AI tool scoping (read & write guards, strict_scope) #195). It also keeps reply_to, as upstream createSentMessage(messageId, postable, threadId, replyTo) does.
  • Errors: Python ChatError has no code, so these are ChatErrors with upstream's exact messages. The upstream reply cross-thread check is a plain Error; here it is a ChatError with the same text. No code attribute is added.

Tests ported

  • thread.test.ts [markAsRead] (7) and [reply()] (9) → tests/test_thread_faithful.py::TestMarkAsRead / ::TestReply, all under their exact names. "converts JSX cards before delegating" is ported as a CardElement dict passthrough with no absorber.
  • bfee00af's postEphemeral assertion changes in thread.test.ts / channel.test.ts: the Python assertions now expect options=PostEphemeralOptions(...).
  • Python-specific tests (an AsyncMock for every hook): a 3-argument custom post_ephemeral still works (thread and channel); an adapter returning None gives None with no DM fallback; edit() after reply() keeps reply_to; edit keeps a thread_id_override; history append; current-message ID resolution; markdown_text/task_update buffering; callback scope on reply cards; mark_as_read(""); a BaseAdapter subclass without the hooks fails first (stream not consumed, no callback token minted, mark_as_read() outside a handler raises "read-receipts"); whitespace-only streams use JS trim() whitespace; create_sent_message_from_message(...).edit() keeps reply_to; tests/test_compat.py covers the probe (positional, keyword-only, **kwargs, positional-only, bound methods, AsyncMock, unhashable callables).

Fidelity (--report-target against chat@4.41.1):

Delta vs committed report (HEAD): missing 118 -> 104 (-14)
  packages/chat/src/thread.test.ts: 15 -> 1 (-14)

thread.test.ts exact matches went from 137 to 153 (+16). The committed report listed 14 of these tests as missing. The other 2 ("delegates a message id…", "falls back to a space…") had been fuzzy-matched to unrelated tests and now match exactly. The one remaining missing test (startTyping) belongs to #201. Strict at the pin: 733/733.

Divergences

  1. post_ephemeral signature probe (one non-parity table row). chat_sdk._compat.accepts_kwarg passes options= only to implementations that take an options parameter (positional-or-keyword or keyword-only) or **kwargs. JavaScript drops extra arguments; Python would raise TypeError for third-party adapters written against the 3-argument signature. The result is cached per function, and bound methods are probed through __func__. [4.41/C7] Turn cancellation: abort_turn, thread.signal, typing options, agent-session events #201 (start_typing(options=)) can reuse it.

ChatError in place of a plain Error/code follows the History API precedent (#197) and is documented in the new section, not as a table row.

Consumer impact

Validation

ruff check / format, audit_test_quality.py (0 hard failures), --check-docs, --strict at chat@4.31.0 (733/733), pytest (7427 passed, 24 skipped, at 4e078bb with origin/main included) and pyrefly check (0 errors) are all green.

Fidelity after the review fixes: Delta vs committed report (HEAD): missing 104 -> 104 (+0) (the new tests are Python-specific).

Merge gate

Independent review findings (5): all addressed.

  • Fixed (major): the raising BaseAdapter.reply / BaseAdapter.mark_as_read defaults made every BaseAdapter subclass look capable. thread.reply(m, stream) drained the stream and minted callback tokens before it raised, and thread.mark_as_read() outside a handler raised MESSAGE_REQUIRED instead of ChatNotImplementedError. Upstream checks the hook before anything else (thread.ts:607-612, :915-920). The defaults are removed; both hooks are now documented on BaseAdapter in a comment block with their signatures, which is how [4.41/C6] Thread.reply, Thread.mark_as_read, post_ephemeral options #200 scoped them. Three regression tests in tests/test_compat.py failed before the fix.
  • Fixed (minor): the missing Known Non-Parity row for the BaseAdapter path. The fix above removes the divergence itself, so no row is needed.
  • Fixed (minor): the empty-stream fallback in reply() now strips JS_WHITESPACE (JS trim()). A parametrized test covers spaces/newline, U+FEFF and U+001C. The same accumulated.strip() pattern in the fallback-stream path is older than this PR and left unchanged.
  • Fixed (minor): test_edit_of_a_wrapped_message_keeps_reply_to covers create_sent_message_from_message(...).edit() keeping reply_to. The mutation is confirmed killed.
  • Fixed (minor): the whitespace-only stream test also catches dropping the .strip() check. The mutation is confirmed killed.

gpt-6-astra: 1 round on 4e078bb. PASS, no actionable findings.

Bots: CodeRabbit was rate-limited and left no comments. There are no gemini comments, so nothing was actionable.

CI: all checks green on 4e078bb (test 3.12/3.13, Lint & Type Check, CodeQL).

Closes #200
Part of #184

…#200)

Ports vercel/chat 83ede7ea (#819), 18d4a230 (#820), the core doc part of
160140e3 (#737) and the core slice of bfee00af (#939):

- Thread.reply(target, message): native replies with reply_to, known-message
  resolution (never fetched), stream buffering, history append
- Thread.mark_as_read(message=None) with upstream error order
- optional BaseAdapter.reply / mark_as_read hooks; Thread Protocol members
- post_ephemeral forwards options= (signature probe keeps 3-arg custom
  adapters working) and may return None
- SentMessage.edit keeps the resolved thread_id and reply_to
- mock adapter records mark_as_read by default

Ports all 16 [markAsRead]/[reply()] upstream tests.
@coderabbitai

coderabbitai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 56 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 4b067790-20d6-461a-91fd-25273e633a16

📥 Commits

Reviewing files that changed from the base of the PR and between 3782945 and 4e078bb.

📒 Files selected for processing (13)
  • CHANGELOG.md
  • docs/UPSTREAM_SYNC.md
  • scripts/fidelity_target.json
  • src/chat_sdk/_compat.py
  • src/chat_sdk/adapters/google_chat/adapter.py
  • src/chat_sdk/adapters/slack/adapter.py
  • src/chat_sdk/channel.py
  • src/chat_sdk/shared/mock_adapter.py
  • src/chat_sdk/thread.py
  • src/chat_sdk/types.py
  • tests/test_channel_faithful.py
  • tests/test_compat.py
  • tests/test_thread_faithful.py
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…S trim for empty-stream check, edit reply_to test

- Remove the concrete BaseAdapter.reply / mark_as_read bodies (documented as
  optional hooks instead) so Thread.reply()/mark_as_read() raise
  ChatNotImplementedError before consuming a stream, minting callback tokens
  or raising MESSAGE_REQUIRED, matching upstream thread.ts fail-first order.
- Empty-stream fallback in Thread.reply strips JS_WHITESPACE (JS trim()).
- Tests: BaseAdapter fail-first regressions, whitespace-only stream,
  create_sent_message_from_message edit keeps reply_to.
@patrick-chinchill
patrick-chinchill marked this pull request as ready for review October 1, 2026 07:12
@patrick-chinchill

Copy link
Copy Markdown
Collaborator Author

Merge gate: CI green (Lint & Type Check, test (3.12), test (3.13), Analyze (python), Analyze (actions), CodeQL) on 4e078bb; local Codex review (gpt-6-astra, xhigh, --base origin/main) on 4e078bb: "No actionable regressions found against the specified merge base. The full test suite passed with 7,427 passed and 24 skipped; targeted lint checks and type checking also passed."; 3 astra rounds; origin/main (3782945) already an ancestor, no re-merge needed; CodeRabbit rate-limited (no review), no inline review comments. Merging with --admin (Protect Main requires a code-owner approval).

@patrick-chinchill
patrick-chinchill merged commit f381f69 into main Oct 1, 2026
8 checks passed
@patrick-chinchill
patrick-chinchill deleted the sync/4.41-c6 branch October 1, 2026 07:16
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.

[4.41/C6] Thread.reply, Thread.mark_as_read, post_ephemeral options

1 participant