feat(swift): add SummarizingChatStorage for automatic history compression - #587
Conversation
…sion — Closes #584 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b6ab5685bc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| try await base.saveMessages( | ||
| compressed, | ||
| userId: userId, sessionId: sessionId, agentId: agentId, | ||
| maxMessages: nil | ||
| ) |
There was a problem hiding this comment.
Replace history instead of appending summaries
When this wrapper is used with any bundled store (FileChatStorage, InMemoryChatStorage, or DeviceChatStorage), saveMessages is append-only rather than replacement. After the cache is invalidated or the app restarts, fetch reads the original long history plus the summary/recent messages, so the history is not actually compressed and can grow with duplicated content. This needs a true replace/delete path or a storage API that supports replacement.
Useful? React with 👍 / 👎.
| if let cached = await cache.get(key: key) { | ||
| return cached |
There was a problem hiding this comment.
Trim cached fetches to the requested maxMessages
Once a compressed result is cached, later fetch calls return it before applying the caller's maxMessages. If one caller caches a larger compressed history and another asks for a tighter window, this violates the ChatStorage.fetch contract that all bundled stores honor by trimming to the requested budget. Return a trimmed view of the cached messages or otherwise account for maxMessages in the cache path.
Useful? React with 👍 / 👎.
| // MARK: - Private | ||
|
|
||
| private func cacheKey(userId: String, sessionId: String, agentId: String) -> String { | ||
| "\(userId)#\(sessionId)#\(agentId)" |
There was a problem hiding this comment.
Use a collision-free cache key
Because the cache key concatenates raw IDs with #, distinct scopes collide whenever any ID contains that separator, e.g. (userId: "a#b", sessionId: "c", agentId: "d") and (userId: "a", sessionId: "b#c", agentId: "d"). After one scope is summarized, the other can receive its cached messages without reading the base store, leaking or mixing chat history across users/sessions/agents. Use a structured key type or escaping rather than raw concatenation.
Useful? React with 👍 / 👎.
All bundled stores (InMemoryChatStorage, FileChatStorage, DeviceChatStorage) append in saveMessages rather than replace. Writing the compressed result back would corrupt history by appending the summary on top of the original messages. Use only the in-memory SummarizationCache actor, consistent with Python and TypeScript implementations. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lisions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…mpression, full docs Rewrites SummarizingChatStorage with a LangChain-inspired design: raw messages always written to the inner store; an in-memory buffer activates lazily on the first qualifying fetch (history > triggerAt * 2). Once active, each save appends to the buffer and compresses immediately if needed — fetch is always fast. fetchAllChats is never intercepted so raw history stays available for analytics and cross-agent routing. Rewrites the test suite (14 tests) to cover the new lifecycle: below/at/above trigger, lazy activation, eager save-time compression, no write-back to base store, fetchAllChats bypass, base storage integrity, and error propagation. Adds documentation: - docs/src/content/docs/storage/summarizing.mdx (cross-language) - docs/src/content/docs/swift/storage/built-in/summarizing.md (Swift deep-dive) - Updates storage overview pages and swift/SKILL.md + swift/README.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Issue Link
Closes #584
Summary
SummarizingChatStorage, aChatStoragewrapper that automatically compresses conversation history when it exceeds a configurable thresholdtriggerAt * 2; user supplies aChatSummarizerclosure (@Sendable,async throws) that receives the full history andkeepLastsaveMessages(replace semantics) and cached in a privateSummarizationCacheactorfetchAllChatsis never intercepted — classifier sees raw historysaveorsaveMessagescallChanges
swift/Sources/AgentSquad/Storage/SummarizingChatStorage.swift— new fileswift/Tests/AgentSquadTests/SummarizingChatStorageTests.swift— 13 testsUser experience
Checklist
🤖 Generated with Claude Code