Skip to content

Commit 42006c6

Browse files
fix: cache PostableObjects in history with real message ID
PostableObject posts (e.g., Plan) now cached in thread/channel message history using the real adapter-returned message ID and fallback text. This is an intentional divergence from upstream TS, which skips history for PostableObjects (a gap, not a design choice — regular posts and streaming posts both cache, only PostableObjects were missed). Changes: - post_postable_object() now returns the RawMessage - Thread/Channel _handle_postable_object returns RawMessage - History append uses raw.id (not synthetic "postable-obj" constant) - Documented as intentional divergence in UPSTREAM_SYNC.md non-parity Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7b3d429 commit 42006c6

4 files changed

Lines changed: 25 additions & 11 deletions

File tree

docs/UPSTREAM_SYNC.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,8 @@ stay explicit instead of being rediscovered in code review.
392392
| `ast_to_plain_text` | Joins blocks with `\n` | Concatenates directly | More readable output |
393393
| `renderPostable` on unknown input | Returns `str(message)` | Throws `Error` | More resilient |
394394
| Chat resolver | 3-level: explicit → ContextVar → global | Process-global singleton | See [DECISIONS.md](DECISIONS.md#why-3-level-chat-resolver) |
395+
| PostableObject history | Cached in message history with real message ID | Not cached (skips history) | Upstream gap — posted messages should appear in thread/channel history |
396+
| Teams `msteams` transport key | Stripped from action values | Not stripped | Upstream gap — SDK-injected metadata should not leak to handlers |
395397

396398
### Platform-specific gaps
397399

src/chat_sdk/channel.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -282,10 +282,13 @@ async def post(
282282
If the message is a PostableObject (e.g. Plan), it is posted via
283283
native adapter support or fallback text.
284284
"""
285-
# Handle PostableObject (e.g. Plan) — returned directly, not cached
286-
# in message history (matches upstream TS behavior).
285+
# Handle PostableObject (e.g. Plan)
287286
if is_postable_object(message):
288-
await self._handle_postable_object(message)
287+
raw = await self._handle_postable_object(message)
288+
if self._message_history is not None and raw is not None:
289+
fallback = message.get_fallback_text() if hasattr(message, "get_fallback_text") else ""
290+
sent = self._create_sent_message(raw.id, PostableMarkdown(markdown=fallback), raw.thread_id)
291+
await self._message_history.append(self._id, _to_message(sent))
289292
return message
290293

291294
if _is_async_iterable(message):
@@ -314,7 +317,7 @@ async def _post_single_message(
314317

315318
return sent
316319

317-
async def _handle_postable_object(self, obj: Any) -> None:
320+
async def _handle_postable_object(self, obj: Any) -> Any:
318321
"""Post a PostableObject using native adapter support or fallback."""
319322
adapter = self.adapter
320323

@@ -323,7 +326,7 @@ async def _post_fn(thread_id: str, message: str) -> Any:
323326
return await adapter.post_channel_message(thread_id, message)
324327
return await adapter.post_message(thread_id, message)
325328

326-
await post_postable_object(obj, adapter, self._id, _post_fn)
329+
return await post_postable_object(obj, adapter, self._id, _post_fn)
327330

328331
async def post_ephemeral(
329332
self,

src/chat_sdk/plan.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,12 @@ async def post_postable_object(
145145
thread_id: str,
146146
post_fn: Any,
147147
logger: Logger | None = None,
148-
) -> None:
148+
) -> Any:
149149
"""Post a PostableObject using the adapter's native support or fallback text.
150150
151+
Returns the ``RawMessage`` from the adapter so callers can use the
152+
real message ID for history caching.
153+
151154
Parameters
152155
----------
153156
obj:
@@ -176,6 +179,7 @@ def _make_context(raw: Any) -> PostableObjectContext:
176179
else:
177180
raw = await post_fn(thread_id, obj.get_fallback_text())
178181
obj.on_posted(_make_context(raw))
182+
return raw
179183

180184

181185
# =============================================================================

src/chat_sdk/thread.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -440,10 +440,15 @@ async def post(
440440
or a PostableObject (e.g. Plan). PostableObjects are returned directly
441441
after posting so the caller can continue to mutate them.
442442
"""
443-
# Handle PostableObject (e.g. Plan) — returned directly, not cached
444-
# in message history (matches upstream TS behavior).
443+
# Handle PostableObject (e.g. Plan)
445444
if is_postable_object(message):
446-
await self._handle_postable_object(message)
445+
raw = await self._handle_postable_object(message)
446+
# Cache in history with the real message ID (upstream skips this,
447+
# but that's a gap — posted messages should appear in history).
448+
if self._message_history is not None and raw is not None:
449+
fallback = message.get_fallback_text() if hasattr(message, "get_fallback_text") else ""
450+
sent = self._create_sent_message(raw.id, PostableMarkdown(markdown=fallback), raw.thread_id)
451+
await self._message_history.append(self._id, _to_message(sent))
447452
return message
448453

449454
# Handle AsyncIterable (streaming)
@@ -459,9 +464,9 @@ async def post(
459464

460465
return result
461466

462-
async def _handle_postable_object(self, obj: Any) -> None:
467+
async def _handle_postable_object(self, obj: Any) -> Any:
463468
"""Post a PostableObject using native adapter support or fallback."""
464-
await post_postable_object(
469+
return await post_postable_object(
465470
obj,
466471
self.adapter,
467472
self._id,

0 commit comments

Comments
 (0)