Repository navigation
feat(core): Thread.reply, Thread.mark_as_read, post_ephemeral options (#200) - #294
Conversation
…#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.
|
Warning Review limit reachedYou'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. View limit detailsLimit details: You’ve used the included review currently available. Review configuration: ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (13)
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. Comment |
# Conflicts: # CHANGELOG.md
…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.
|
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). |
Summary
Adds the core thread APIs from chat@4.35–4.41:
Thread.reply(target, message),Thread.mark_as_read(message=None), andpost_ephemeralforwarding the caller'sPostEphemeralOptions(an adapter may now returnNone). 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
83ede7eafeat(chat): add message reply support (#819, chat@4.38.0)ThreadImpl.reply,_find_known_message,_create_sent_message(reply_to=), edit keepsthread_id+reply_to,replydocumented as an optional hook onBaseAdapter(no default),ThreadProtocolreply18d4a230feat(chat): add mark as read support (#820, chat@4.38.0)ThreadImpl.mark_as_read,mark_as_readdocumented as an optional hook onBaseAdapter(no default),ThreadProtocolmark_as_read, mock adaptermark_as_read160140e3feat(teams): targeted ephemeral (#737, chat@4.35.0)BaseAdapter.post_ephemeraldocstring); Teams half is #219bfee00affeat(gmail) (#939, chat@4.41.0), core slicepost_ephemeral(..., *, options=None) -> EphemeralMessage | None;Thread/Channel.post_ephemeralforwardoptions; Slack and Google Chat accept and ignore itBehavior
reply(): in upstream order, checks for the hook, resolves the target (current message, thenrecent_messages; never fetched), rejects a cross-threadMessage, buffers a stream throughfrom_full_streamintoPostableMarkdown(" "when there is no text), processes callback URLs (thread scope), callsadapter.reply, builds aSentMessagewithreply_to, then appends to history.mark_as_read(): errors in upstream order (no hook →MESSAGE_REQUIRED→THREAD_MISMATCH).Nonemeans the current message, checked withis None. An explicit""is rejected asMESSAGE_REQUIRED, as upstream ("" ?? currentthen!target).SentMessage.edit()now keeps the resolvedthread_id. Before, a message posted under athread_id_overridereverted toself._idon edit (handed off from [4.41/C3] Conversation context + AI tool scoping (read & write guards, strict_scope) #195). It also keepsreply_to, as upstreamcreateSentMessage(messageId, postable, threadId, replyTo)does.ChatErrorhas nocode, so these areChatErrors with upstream's exact messages. The upstream reply cross-thread check is a plainError; here it is aChatErrorwith the same text. Nocodeattribute 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 aCardElementdict passthrough with no absorber.bfee00af'spostEphemeralassertion changes inthread.test.ts/channel.test.ts: the Python assertions now expectoptions=PostEphemeralOptions(...).AsyncMockfor every hook): a 3-argument custompost_ephemeralstill works (thread and channel); an adapter returningNonegivesNonewith no DM fallback;edit()afterreply()keepsreply_to; edit keeps athread_id_override; history append; current-message ID resolution;markdown_text/task_updatebuffering; callback scope on reply cards;mark_as_read(""); aBaseAdaptersubclass 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 JStrim()whitespace;create_sent_message_from_message(...).edit()keepsreply_to;tests/test_compat.pycovers the probe (positional, keyword-only,**kwargs, positional-only, bound methods,AsyncMock, unhashable callables).Fidelity (
--report-targetagainst chat@4.41.1):thread.test.tsexact 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
post_ephemeralsignature probe (one non-parity table row).chat_sdk._compat.accepts_kwargpassesoptions=only to implementations that take anoptionsparameter (positional-or-keyword or keyword-only) or**kwargs. JavaScript drops extra arguments; Python would raiseTypeErrorfor 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.ChatErrorin place of a plainError/codefollows the History API precedent (#197) and is documented in the new section, not as a table row.Consumer impact
Thread.post_ephemeral/Channel.post_ephemeralnow passoptions=to adapters that accept it. Tests that assert a three-argument adapter call needoptions=...added (chinchill: check anypost_ephemeral.assert_called_*).thread.reply()/thread.mark_as_read()raiseChatNotImplementedErroron adapters without the hook. No in-repo adapter hasreplyuntil [4.41/TG4] Telegram replies: replied-to context, reply-to-bot as mention, native replies, portable file data #228/[4.41/W4] WhatsApp & Messenger: mark_as_read, native replies, code fences, guarded downloads #239. On WhatsApp,thread.mark_as_read()raisesTypeErroruntil [4.41/W4] WhatsApp & Messenger: mark_as_read, native replies, code fences, guarded downloads #239 (its one-argumentmark_as_read(message_id)is unchanged).chat_sdk.testing.create_mock_adapter()now has a recordingmark_as_readAsyncMock.Validation
ruff check / format,
audit_test_quality.py(0 hard failures),--check-docs,--strictat chat@4.31.0 (733/733), pytest (7427 passed, 24 skipped, at4e078bbwith origin/main included) andpyrefly 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.
BaseAdapter.reply/BaseAdapter.mark_as_readdefaults made everyBaseAdaptersubclass look capable.thread.reply(m, stream)drained the stream and minted callback tokens before it raised, andthread.mark_as_read()outside a handler raisedMESSAGE_REQUIREDinstead ofChatNotImplementedError. Upstream checks the hook before anything else (thread.ts:607-612, :915-920). The defaults are removed; both hooks are now documented onBaseAdapterin 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 intests/test_compat.pyfailed before the fix.reply()now stripsJS_WHITESPACE(JStrim()). A parametrized test covers spaces/newline, U+FEFF and U+001C. The sameaccumulated.strip()pattern in the fallback-stream path is older than this PR and left unchanged.test_edit_of_a_wrapped_message_keeps_reply_tocoverscreate_sent_message_from_message(...).edit()keepingreply_to. The mutation is confirmed killed..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