Skip to content

Commit c59e6cd

Browse files
committed
feat(chat): Transcripts API + rename message history cache to thread_history (vercel/chat#448)
Port of upstream 46d183b (chat@4.29.0). Rename (with back-compat, mirroring upstream): - message_history.py -> thread_history.py; MessageHistoryCache -> ThreadHistoryCache. Old module path kept as a deprecated re-export shim. - ChatConfig.thread_history added; deprecated ChatConfig.message_history still read, thread_history wins when both are set. - Adapter.persist_thread_history added; deprecated persist_message_history still honored (either flag enables persistence). Telegram and WhatsApp adapters switch to the new flag, matching upstream. - State storage key prefix "msg-history:" is deliberately unchanged so existing persisted data is not orphaned. New Transcripts API: - transcripts.py: TranscriptsApiImpl (append/list/count/delete) keyed by a cross-platform user key, backed by StateAdapter.append_to_list. delete() writes a tombstone via append_to_list(max_length=1) because state.delete only addresses the k/v namespace on non-memory adapters. - ChatConfig.transcripts + ChatConfig.identity (IdentityResolver); the constructor raises when transcripts is set without identity. Inbound dispatch resolves message.user_key once per message via the resolver. - chat.transcripts accessor raises when not configured (fail loudly). - New types: TranscriptEntry, TranscriptsConfig, TranscriptRole, AppendInput, AppendOptions, ListQuery, CountQuery, DeleteTarget, DeleteResult, IdentityContext, IdentityResolver, DurationString. Tests: test_message_history.py renamed to test_thread_history.py (names aligned to thread-history.test.ts titles, deprecated-alias test added); chat.test.ts persistThreadHistory block ported into test_chat_faithful.py; new test_transcripts.py and test_transcripts_wiring.py port transcripts.test.ts and transcripts-wiring.test.ts 1:1. https://claude.ai/code/session_013zwTcMek5rNqBTQvs2oF64 (cherry picked from commit 765420dc4d21804b19f1d8eccddebc3a392b0e61)
1 parent 731e37c commit c59e6cd

22 files changed

Lines changed: 1683 additions & 215 deletions

src/chat_sdk/__init__.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@
6565
from chat_sdk.errors import ChatError, ChatNotImplementedError, LockError, RateLimitError, StateNotConnectedError
6666
from chat_sdk.from_full_stream import from_full_stream
6767
from chat_sdk.logger import ConsoleLogger, Logger, LogLevel
68+
69+
# Deprecated aliases — renamed to ThreadHistoryCache / ThreadHistoryConfig.
6870
from chat_sdk.message_history import MessageHistoryCache, MessageHistoryConfig
6971
from chat_sdk.modals import (
7072
ExternalSelect,
@@ -121,10 +123,13 @@
121123
from chat_sdk.shared.streaming_markdown import StreamingMarkdownRenderer
122124
from chat_sdk.state.memory import MemoryStateAdapter
123125
from chat_sdk.thread import ThreadImpl
126+
from chat_sdk.thread_history import ThreadHistoryCache, ThreadHistoryConfig
124127
from chat_sdk.types import (
125128
ActionEvent,
126129
Adapter,
127130
AdapterPostableMessage,
131+
AppendInput,
132+
AppendOptions,
128133
AppHomeOpenedEvent,
129134
AssistantContextChangedEvent,
130135
AssistantThreadStartedEvent,
@@ -138,6 +143,10 @@
138143
ChatInstance,
139144
ConcurrencyConfig,
140145
ConcurrencyStrategy,
146+
CountQuery,
147+
DeleteResult,
148+
DeleteTarget,
149+
DurationString,
141150
EmojiFormats,
142151
EmojiValue,
143152
EphemeralMessage,
@@ -146,7 +155,10 @@
146155
FetchResult,
147156
FileUpload,
148157
FormattedContent,
158+
IdentityContext,
159+
IdentityResolver,
149160
LinkPreview,
161+
ListQuery,
150162
ListThreadsOptions,
151163
ListThreadsResult,
152164
Lock,
@@ -185,6 +197,10 @@
185197
Thread,
186198
ThreadInfo,
187199
ThreadSummary,
200+
TranscriptEntry,
201+
TranscriptRole,
202+
TranscriptsApi,
203+
TranscriptsConfig,
188204
UserInfo,
189205
WebhookOptions,
190206
WellKnownEmoji,
@@ -298,7 +314,10 @@
298314
"ConsoleLogger",
299315
"Logger",
300316
"LogLevel",
301-
# Message history
317+
# Thread history (per-thread cache)
318+
"ThreadHistoryCache",
319+
"ThreadHistoryConfig",
320+
# Thread history — deprecated aliases (renamed upstream)
302321
"MessageHistoryCache",
303322
"MessageHistoryConfig",
304323
# Modal builders (PascalCase primary — matches source TS SDK)
@@ -335,6 +354,8 @@
335354
"ActionEvent",
336355
"Adapter",
337356
"AdapterPostableMessage",
357+
"AppendInput",
358+
"AppendOptions",
338359
"AppHomeOpenedEvent",
339360
"AssistantContextChangedEvent",
340361
"AssistantThreadStartedEvent",
@@ -348,6 +369,10 @@
348369
"ChatInstance",
349370
"ConcurrencyConfig",
350371
"ConcurrencyStrategy",
372+
"CountQuery",
373+
"DeleteResult",
374+
"DeleteTarget",
375+
"DurationString",
351376
"EmojiFormats",
352377
"EmojiValue",
353378
"EphemeralMessage",
@@ -356,7 +381,10 @@
356381
"FetchResult",
357382
"FileUpload",
358383
"FormattedContent",
384+
"IdentityContext",
385+
"IdentityResolver",
359386
"LinkPreview",
387+
"ListQuery",
360388
"ListThreadsOptions",
361389
"ListThreadsResult",
362390
"Lock",
@@ -396,6 +424,10 @@
396424
"Thread",
397425
"ThreadInfo",
398426
"ThreadSummary",
427+
"TranscriptEntry",
428+
"TranscriptRole",
429+
"TranscriptsApi",
430+
"TranscriptsConfig",
399431
"UserInfo",
400432
"WebhookOptions",
401433
"WellKnownEmoji",

src/chat_sdk/adapters/teams/adapter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1700,7 +1700,7 @@ async def _close_stream_session(
17001700
# from typing indicator to message bubble; if that send fails
17011701
# the streaming UI may stay until Teams times the session out
17021702
# client-side, but the recorded ``SentMessage`` and
1703-
# ``_message_history`` entry still match what the user saw.
1703+
# ``_thread_history`` entry still match what the user saw.
17041704
self._logger.warn(
17051705
"Teams stream final activity failed",
17061706
{"threadId": thread_id, "error": str(exc)},

src/chat_sdk/adapters/telegram/adapter.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -594,7 +594,7 @@ def __init__(self, config: TelegramAdapterConfig | None = None) -> None:
594594

595595
self._name: str = "telegram"
596596
self._lock_scope: LockScope = "channel"
597-
self._persist_message_history: bool = True
597+
self._persist_thread_history: bool = True
598598

599599
self._bot_token: str = bot_token
600600
self._api_base_url: str = _trim_trailing_slashes(
@@ -640,8 +640,8 @@ def lock_scope(self) -> LockScope:
640640
return self._lock_scope
641641

642642
@property
643-
def persist_message_history(self) -> bool:
644-
return self._persist_message_history
643+
def persist_thread_history(self) -> bool:
644+
return self._persist_thread_history
645645

646646
@property
647647
def bot_user_id(self) -> str | None:

src/chat_sdk/adapters/whatsapp/adapter.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ class WhatsAppAdapter:
112112
def __init__(self, config: WhatsAppAdapterConfig) -> None:
113113
self._name = "whatsapp"
114114
self._lock_scope: LockScope = "channel"
115-
self._persist_message_history = True
115+
self._persist_thread_history = True
116116
self._user_name = config.user_name
117117
self._access_token = config.access_token
118118
self._app_secret = config.app_secret
@@ -137,8 +137,8 @@ def lock_scope(self) -> LockScope:
137137
return self._lock_scope
138138

139139
@property
140-
def persist_message_history(self) -> bool:
141-
return self._persist_message_history
140+
def persist_thread_history(self) -> bool:
141+
return self._persist_thread_history
142142

143143
@property
144144
def user_name(self) -> str:

src/chat_sdk/channel.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ class _ChannelImplConfigWithAdapter:
8383
state_adapter: StateAdapter
8484
channel_visibility: ChannelVisibility = "unknown"
8585
is_dm: bool = False
86-
message_history: Any = None
86+
thread_history: Any = None
8787

8888

8989
@dataclass
@@ -122,15 +122,15 @@ def __init__(self, config: _ChannelImplConfig) -> None:
122122
self._adapter: Adapter | None = None
123123
self._adapter_name: str | None = config.adapter_name
124124
self._state_adapter_instance: StateAdapter | None = None
125-
self._message_history: Any = None
125+
self._thread_history: Any = None
126126
else:
127127
# _ChannelImplConfigWithAdapter, _ChannelImplConfigForThread,
128128
# or _ChannelImplConfigForChat (from chat.py) -- all have
129-
# adapter, state_adapter, and optional message_history attrs.
129+
# adapter, state_adapter, and optional thread_history attrs.
130130
self._adapter = config.adapter # type: ignore[union-attr]
131131
self._adapter_name = None
132132
self._state_adapter_instance = config.state_adapter # type: ignore[union-attr]
133-
self._message_history = getattr(config, "message_history", None)
133+
self._thread_history = getattr(config, "thread_history", None)
134134

135135
# -- Properties ----------------------------------------------------------
136136

@@ -206,7 +206,7 @@ async def messages(self) -> AsyncIterator[Message]:
206206
"""
207207
adapter = self.adapter
208208
channel_id = self._id
209-
message_history = self._message_history
209+
thread_history = self._thread_history
210210
cursor: str | None = None
211211
yielded_any = False
212212

@@ -227,8 +227,8 @@ async def messages(self) -> AsyncIterator[Message]:
227227
cursor = result.next_cursor
228228

229229
# Fallback to cached history
230-
if not yielded_any and message_history is not None:
231-
cached: list[Message] = await message_history.get_messages(channel_id)
230+
if not yielded_any and thread_history is not None:
231+
cached: list[Message] = await thread_history.get_messages(channel_id)
232232
for msg in reversed(cached):
233233
yield msg
234234

@@ -285,10 +285,10 @@ async def post(
285285
# Handle PostableObject (e.g. Plan)
286286
if is_postable_object(message):
287287
raw = await self._handle_postable_object(message)
288-
if self._message_history is not None and raw is not None:
288+
if self._thread_history is not None and raw is not None:
289289
fallback = message.get_fallback_text() if hasattr(message, "get_fallback_text") else ""
290290
sent = self._create_sent_message(raw.id, PostableMarkdown(markdown=fallback), raw.thread_id)
291-
await self._message_history.append(self._id, _to_message(sent))
291+
await self._thread_history.append(self._id, _to_message(sent))
292292
return message
293293

294294
if _is_async_iterable(message):
@@ -312,8 +312,8 @@ async def _post_single_message(
312312

313313
sent = self._create_sent_message(raw_msg.id, postable, raw_msg.thread_id)
314314

315-
if self._message_history is not None:
316-
await self._message_history.append(self._id, _to_message(sent))
315+
if self._thread_history is not None:
316+
await self._thread_history.append(self._id, _to_message(sent))
317317

318318
return sent
319319

@@ -439,11 +439,11 @@ def from_json(
439439

440440
# Validation passed — safe to invalidate.
441441
# Both `_state_adapter_instance` (old chat's state backend)
442-
# and `_message_history` (old chat's cache) would route to
442+
# and `_thread_history` (old chat's cache) would route to
443443
# the previous context otherwise.
444444
if adapter is not None or chat is not None:
445445
channel._state_adapter_instance = None
446-
channel._message_history = None
446+
channel._thread_history = None
447447
else:
448448
# Explicit None-checks (not `or`) to avoid the truthiness trap:
449449
# `""` is a valid-but-falsy value that shouldn't silently fall

0 commit comments

Comments
 (0)