You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[4.41/C8] Unified History API (bot.history.user/thread/channel), to_prompt_entries, transcripts deprecation #197
Upstream makes chat.history the canonical history API with three scopes: user (the old chat.transcripts), thread (list/collect/append) and channel (list_messages/list_threads/list_threads_with_messages). transcripts/TranscriptEntry stay as deprecated aliases, AI read tools route through history, and max_per_user=False disables count eviction. Python has only chat.transcripts.
Upstream changes
169788b6 feat(chat): introduce unified History API with user, thread, and chan… (#592) — chat@4.39.0:
ChatConfig.history = {thread?, user?}; thread precedence history.thread ?? threadHistory ?? messageHistory; user config merged {...transcripts, ...history.user}; identity history.user.identity ?? identity, construction throws without one.
chat.transcripts returns history.user; TranscriptsApiImpl re-exports UserHistoryApiImpl; HistoryEntry/UserHistoryEntry, toPromptEntries. AI fetchMessages/fetchChannelMessages/listThreads call chat.history.*; unregistered prefixes and missing capabilities throw.
056d8830 feat(history): support uncapped per-user retention (#904) — chat@4.41.0 — maxPerUser?: number | false on both UserHistoryConfig and TranscriptsConfig. false means no cap.
Current Python behavior
grep -rnE 'HistoryApi|HistoryConfig|to_prompt_entries|PromptEntry|HistoryEntry|UserHistory' src tests only hits the unrelated ThreadHistoryConfig (thread_history.py:35).
src/chat_sdk/chat.py:379-393 builds _ThreadHistoryCache (:2688, append/get_messages) from thread_history/message_history and TranscriptsApiImpl from config.transcripts + config.identity (raising ValueError without identity); :446-456 is the transcripts property.
src/chat_sdk/types.py:1732 has ChatConfig.thread_history: dict (:1721 deprecated message_history), :1739 has transcripts, and :1713 has identity. TranscriptsConfig.max_per_user: int | None is at :2062.
src/chat_sdk/transcripts.py:98 maps None to 200; False passes through to append_to_list, uncapped only by accident (state/memory.py:215if max_length and …, state/redis.py:265max_length or 0, state/postgres.py:386if max_length:).
src/chat_sdk/ai/tools.py: fetch_messages (:631) calls thread.adapter.fetch_messages with no SDK-cache fallback for persisting adapters; fetch_channel_messages (:682) and list_threads (:761) resolve the adapter by hand and raise Adapter "x" does not support ….
ChatInstance (types.py:1748) exposes transcripts but not history.
Scope
New package src/chat_sdk/history/:
resolve_adapter.py: require_adapter(get_adapter, id, scope) raises ChatError with upstream's text. persists_history(adapter) returns persist_thread_history or persist_message_history.
thread.pyThreadHistoryApiImpl: list falls back to the cache only when the page is empty, next_cursor is None, no cursor was requested, a cache exists and the adapter persists history (window: newest N backward, oldest N forward); collect is an async generator (page size max(1, min(100, remaining)), stops on an empty page even with a cursor, limit == 0 yields nothing, cache path yields oldest N, only when nothing was yielded); append raises without a cache.
channel.pyChannelHistoryApiImpl: list_messages uses fetch_channel_messages, else the channel-keyed cache for persisting adapters, else a capability error; list_threads raises if absent; list_threads_with_messages(max_threads=5, messages_per_thread=None, cursor=None) fetches in batches of 4 via history.thread.list.
user.pyUserHistoryApiImpl: move the body of transcripts.py here. Its append error text becomes history.user.append: options.userKey is required when appending an AppendInput.
to_prompt.pyto_prompt_entries(entries) -> list[PromptEntry], where PromptEntry is a TypedDict(role, content).
__init__.pyHistoryApiImpl(adapter_resolver, cache=None, user=None). Accessing .user raises upstream's "chat.history.user is not configured …" error.
transcripts.py becomes a deprecated alias module: TranscriptsApiImpl = UserHistoryApiImpl (same object).
types.py: HistoryConfig(thread: dict | None, user: UserHistoryConfig | None); UserHistoryConfig(identity, max_per_user, retention, store_formatted) with every field defaulting to None ("unset"); max_per_user: int | Literal[False] | None on both configs; aliases HistoryEntry = TranscriptEntry, UserHistoryEntry, UserHistoryRole; Protocols HistoryApi/ThreadHistoryApi/ChannelHistoryApi, UserHistoryApi = TranscriptsApi; ChatConfig.history, ChatInstance.history.
chat.py: thread-cache precedence with is not None; field-by-field user-config merge; resolve identity (history.user.identity, else identity); if none, raise at construction with upstream's message ("ChatConfig requires an identity resolver when user history (or legacy transcripts) is configured …"), keeping today's ValueError class; self._history takes a resolver closing over self._adapters (sees later registrations); add the history property; transcripts returns history.user.
Route the AI tools fetch_messages, fetch_channel_messages and list_threads through chat.history. Keep the existing translation of ChatNotImplementedError to ChatError. Update the error-text assertions in tests/test_ai_tools.py:479-570.
Export the new names from chat_sdk/__init__.py. Keep the state key prefixes transcripts:user: and msg-history: byte-identical.
Field-by-field merge. Dataclass fields always exist, so {**transcripts, **history.user} can't tell "unset" from "default". With all-NoneUserHistoryConfig defaults, merge per field: u.f if u is not None and u.f is not None else t.f (legacy store_formatted=False is a fine fallback).
max_per_user=False.bool subclasses int and is falsy: normalize first, None if v is False else (v if v is not None else 200), and pass max_length=None; never if not v / isinstance(v, int) before the is False check. max_per_user=0 is uncapped via backend truthiness, as upstream — document, don't change.
Cursor/limit. The list fallback gate uses options.cursor is None / next_cursor is None (upstream === undefined); collect's loop exit is upstream's falsy check (if not result.next_cursor or not result.messages: break), so "" also ends it. limit is None checks; limit=0 yields nothing.
Async.collect never pre-schedules a page, so an early break leaves no fetch pending. list_threads_with_messages uses asyncio.gather per slice of 4, preserving order.
"Absent" capability. Upstream tests if (adapter.fetchChannelMessages). Python BaseAdapter subclasses inherit stubs that raise ChatNotImplementedError (no in-repo adapter subclasses it). Recommended: treat a method as absent when getattr returns Noneor it is the unoverridden BaseAdapter stub, so a persisting adapter still gets the channel-cache fallback exactly as upstream; if a call still raises ChatNotImplementedError, re-raise as the capability error (no cache retry).
Tests
New upstream files under packages/chat/src/history/. #185 adds TARGET_MAPPING rows (history/user → tests/test_transcripts.py for now; the others → files this PR creates). Create tests/test_history_{thread,channel,to_prompt,user}.py and update those rows to match.
to-prompt.test.ts: "maps transcript entries to prompt entries preserving order", "skips entries with empty text".
user.test.ts: the renamed transcripts suite (25 exact name matches today). Move those cases from tests/test_transcripts.py to tests/test_history_user.py, re-pointed at history.user, and add the it.each "retains $expected entries with maxPerUser=$maxPerUser" for False → 205 and default → 200.
Mapped existing files:
transcripts.test.ts now contains only "re-exports UserHistoryApiImpl under the legacy name". Assert TranscriptsApiImpl is UserHistoryApiImpl.
transcripts-wiring.test.ts: add "throws at construction when history.user is set without identity", "chat.transcripts getter throws when user history was not configured", "chat.history.user returns the API instance when configured via history.user", and the it.each "merges legacy maxPerUser=%s under history.user" (50, False). The existing test_chattranscripts_getter_throws_when_transcripts_was_not_configured (:104) fuzzy-absorbs "chat.history.user getter throws when user history was not configured"; retarget it to chat.history.user under that exact name.
ai/index.test.ts (mapped): "fetchChannelMessages throws when the adapter does not support it" keeps does not support fetching channel messages and adds fetch_messages not-called; "listThreads throws when the adapter does not support it" now matches does not implement listThreads.
Acceptance criteria
The full validation command from CLAUDE.md passes (ruff check, ruff format --check, audit_test_quality, verify_test_fidelity, pytest).
docs/UPSTREAM_SYNC.md records the dataclass merge semantics, max_per_user=0 being uncapped (parity), and the BaseAdapter-stub / ChatNotImplementedError capability rule.
CHANGELOG entry under "Unreleased (4.41 wave)": chat.history (additive; transcripts/TranscriptEntry deprecated but working); max_per_user=False; changed AI-tool error text; AI reads return SDK-cached history for persisting adapters (e.g. Telegram/WhatsApp); unknown adapter prefixes now raise.
chat.transcripts is chat.history.user when configured. Existing state keys are unchanged.
Dependencies
Blocked by #195, because the read tools must keep the scope guard ahead of history calls. Blocks #198 and #203.
Metadata
Effort: L (about 1,000–1,400 LOC incl. tests; user-scope tests already exist)
Consumer impact: low. Additive; transcripts keeps working; AI-tool users see new error strings. None for Slack/Teams streaming.
Summary
Upstream makes
chat.historythe canonical history API with three scopes:user(the oldchat.transcripts),thread(list/collect/append) andchannel(list_messages/list_threads/list_threads_with_messages).transcripts/TranscriptEntrystay as deprecated aliases, AI read tools route through history, andmax_per_user=Falsedisables count eviction. Python has onlychat.transcripts.Upstream changes
169788b6feat(chat): introduce unified History API with user, thread, and chan… (#592) — chat@4.39.0:packages/chat/src/history/{index,user,thread,channel,to-prompt,resolve-adapter,types}.ts.ChatConfig.history = {thread?, user?}; thread precedencehistory.thread ?? threadHistory ?? messageHistory; user config merged{...transcripts, ...history.user}; identityhistory.user.identity ?? identity, construction throws without one.chat.transcriptsreturnshistory.user;TranscriptsApiImplre-exportsUserHistoryApiImpl;HistoryEntry/UserHistoryEntry,toPromptEntries. AIfetchMessages/fetchChannelMessages/listThreadscallchat.history.*; unregistered prefixes and missing capabilities throw.056d8830feat(history): support uncapped per-user retention (#904) — chat@4.41.0 —maxPerUser?: number | falseon bothUserHistoryConfigandTranscriptsConfig.falsemeans no cap.Current Python behavior
grep -rnE 'HistoryApi|HistoryConfig|to_prompt_entries|PromptEntry|HistoryEntry|UserHistory' src testsonly hits the unrelatedThreadHistoryConfig(thread_history.py:35).src/chat_sdk/chat.py:379-393builds_ThreadHistoryCache(:2688,append/get_messages) fromthread_history/message_historyandTranscriptsApiImplfromconfig.transcripts+config.identity(raisingValueErrorwithout identity);:446-456is thetranscriptsproperty.src/chat_sdk/types.py:1732hasChatConfig.thread_history: dict(:1721deprecatedmessage_history),:1739hastranscripts, and:1713hasidentity.TranscriptsConfig.max_per_user: int | Noneis at:2062.src/chat_sdk/transcripts.py:98mapsNoneto 200;Falsepasses through toappend_to_list, uncapped only by accident (state/memory.py:215if max_length and …,state/redis.py:265max_length or 0,state/postgres.py:386if max_length:).src/chat_sdk/ai/tools.py:fetch_messages(:631) callsthread.adapter.fetch_messageswith no SDK-cache fallback for persisting adapters;fetch_channel_messages(:682) andlist_threads(:761) resolve the adapter by hand and raiseAdapter "x" does not support ….ChatInstance(types.py:1748) exposestranscriptsbut nothistory.Scope
src/chat_sdk/history/:resolve_adapter.py:require_adapter(get_adapter, id, scope)raisesChatErrorwith upstream's text.persists_history(adapter)returnspersist_thread_history or persist_message_history.thread.pyThreadHistoryApiImpl:listfalls back to the cache only when the page is empty,next_cursor is None, no cursor was requested, a cache exists and the adapter persists history (window: newest N backward, oldest N forward);collectis an async generator (page sizemax(1, min(100, remaining)), stops on an empty page even with a cursor,limit == 0yields nothing, cache path yields oldest N, only when nothing was yielded);appendraises without a cache.channel.pyChannelHistoryApiImpl:list_messagesusesfetch_channel_messages, else the channel-keyed cache for persisting adapters, else a capability error;list_threadsraises if absent;list_threads_with_messages(max_threads=5, messages_per_thread=None, cursor=None)fetches in batches of 4 viahistory.thread.list.user.pyUserHistoryApiImpl: move the body oftranscripts.pyhere. Itsappenderror text becomeshistory.user.append: options.userKey is required when appending an AppendInput.to_prompt.pyto_prompt_entries(entries) -> list[PromptEntry], wherePromptEntryis aTypedDict(role, content).__init__.pyHistoryApiImpl(adapter_resolver, cache=None, user=None). Accessing.userraises upstream's "chat.history.user is not configured …" error.transcripts.pybecomes a deprecated alias module:TranscriptsApiImpl = UserHistoryApiImpl(same object).types.py:HistoryConfig(thread: dict | None, user: UserHistoryConfig | None);UserHistoryConfig(identity, max_per_user, retention, store_formatted)with every field defaulting toNone("unset");max_per_user: int | Literal[False] | Noneon both configs; aliasesHistoryEntry = TranscriptEntry,UserHistoryEntry,UserHistoryRole; ProtocolsHistoryApi/ThreadHistoryApi/ChannelHistoryApi,UserHistoryApi = TranscriptsApi;ChatConfig.history,ChatInstance.history.chat.py: thread-cache precedence withis not None; field-by-field user-config merge; resolve identity (history.user.identity, elseidentity); if none, raise at construction with upstream's message ("ChatConfig requires an identity resolver when user history (or legacytranscripts) is configured …"), keeping today'sValueErrorclass;self._historytakes a resolver closing overself._adapters(sees later registrations); add thehistoryproperty;transcriptsreturnshistory.user.fetch_messages,fetch_channel_messagesandlist_threadsthroughchat.history. Keep the existing translation ofChatNotImplementedErrortoChatError. Update the error-text assertions intests/test_ai_tools.py:479-570.chat_sdk/__init__.py. Keep the state key prefixestranscripts:user:andmsg-history:byte-identical.Out of scope
history.*call.to_ai_messageschanges → [4.41/C9] AI messages: keep attachment/link-only messages, framework-agnostic tool specs #198.Porting notes
{**transcripts, **history.user}can't tell "unset" from "default". With all-NoneUserHistoryConfigdefaults, merge per field:u.f if u is not None and u.f is not None else t.f(legacystore_formatted=Falseis a fine fallback).max_per_user=False.boolsubclassesintand is falsy: normalize first,None if v is False else (v if v is not None else 200), and passmax_length=None; neverif not v/isinstance(v, int)before theis Falsecheck.max_per_user=0is uncapped via backend truthiness, as upstream — document, don't change.listfallback gate usesoptions.cursor is None/next_cursor is None(upstream=== undefined);collect's loop exit is upstream's falsy check (if not result.next_cursor or not result.messages: break), so""also ends it.limit is Nonechecks;limit=0yields nothing.collectnever pre-schedules a page, so an earlybreakleaves no fetch pending.list_threads_with_messagesusesasyncio.gatherper slice of 4, preserving order.if (adapter.fetchChannelMessages). PythonBaseAdaptersubclasses inherit stubs that raiseChatNotImplementedError(no in-repo adapter subclasses it). Recommended: treat a method as absent whengetattrreturnsNoneor it is the unoverriddenBaseAdapterstub, so a persisting adapter still gets the channel-cache fallback exactly as upstream; if a call still raisesChatNotImplementedError, re-raise as the capability error (no cache retry).Tests
New upstream files under
packages/chat/src/history/. #185 addsTARGET_MAPPINGrows (history/user→tests/test_transcripts.pyfor now; the others → files this PR creates). Createtests/test_history_{thread,channel,to_prompt,user}.pyand update those rows to match.thread.test.ts(15) andchannel.test.ts(8): port everyit()verbatim — all are new, e.g. "list does not substitute the cache on a continuation page", "collect stops when a page is empty even if the adapter echoes a cursor", "listThreadsWithMessages bounds per-thread fetch concurrency".verify_test_fidelity.py --report-target([4.41/P0] Fidelity tooling for the 4.41 wave: single pin constant, SHA pin, it.each expansion, map new core test files #185) enumerates any still missing.to-prompt.test.ts: "maps transcript entries to prompt entries preserving order", "skips entries with empty text".user.test.ts: the renamed transcripts suite (25 exact name matches today). Move those cases fromtests/test_transcripts.pytotests/test_history_user.py, re-pointed athistory.user, and add theit.each"retains $expected entries with maxPerUser=$maxPerUser" forFalse→ 205 and default → 200.Mapped existing files:
transcripts.test.tsnow contains only "re-exports UserHistoryApiImpl under the legacy name". AssertTranscriptsApiImpl is UserHistoryApiImpl.transcripts-wiring.test.ts: add "throws at construction when history.user is set without identity", "chat.transcripts getter throws when user history was not configured", "chat.history.user returns the API instance when configured via history.user", and theit.each"merges legacy maxPerUser=%s under history.user" (50,False). The existingtest_chattranscripts_getter_throws_when_transcripts_was_not_configured(:104) fuzzy-absorbs "chat.history.user getter throws when user history was not configured"; retarget it tochat.history.userunder that exact name.ai/index.test.ts(mapped): "fetchChannelMessages throws when the adapter does not support it" keepsdoes not support fetching channel messagesand addsfetch_messagesnot-called; "listThreads throws when the adapter does not support it" now matchesdoes not implement listThreads.Acceptance criteria
docs/UPSTREAM_SYNC.mdrecords the dataclass merge semantics,max_per_user=0being uncapped (parity), and theBaseAdapter-stub /ChatNotImplementedErrorcapability rule.chat.history(additive;transcripts/TranscriptEntrydeprecated but working);max_per_user=False; changed AI-tool error text; AI reads return SDK-cached history for persisting adapters (e.g. Telegram/WhatsApp); unknown adapter prefixes now raise.chat.transcripts is chat.history.userwhen configured. Existing state keys are unchanged.Dependencies
Blocked by #195, because the read tools must keep the scope guard ahead of history calls. Blocks #198 and #203.
Metadata
transcriptskeeps working; AI-tool users see new error strings. None for Slack/Teams streaming.sync/4.41-c8Part of #184.