S3SessionManager abandoned; conversation history written manually to S3

dropthejase wired Strands' built-in `S3SessionManager`, then ripped it out after it failed to give the control needed over history reload - specifically, tool activity cards and reasoning blocks were lost on page refresh. The replacement writes `agent.messages` directly to S3 after each invocation and reads them back before the next turn.

chat-uiinfrastructure

The first iteration wires SessionManager with S3Storage pointing at a sessions bucket. Each chat turn passes a sessionId through the agent factory. This approach stores the full Strands conversation snapshot, but when the backend rehydrates history for the frontend it can't reconstruct tool activity events or reasoning blocks from the baked snapshot - they get stripped or misformatted.

The replacement (efdae23) drops SessionManager entirely. A loadMessages() function reads conversations/{chatId}/messages.json from S3 before each turn and passes the deserialized MessageData[] into the agent constructor as messages: previousMessages. An AfterInvocationEvent hook writes agent.messages back to S3 after each turn. The system prompt is excluded from the snapshot and always loaded fresh from code - this was a specific failure mode of the baked-snapshot approach.

The session ID protocol on the SSE stream is a separate concern and stays: the frontend generates a {userId}-{uuid} session ID on the first turn and immediately PUTs it to PUT /chat/:chatId/session-id when the chat_id SSE event arrives - not after the stream completes, to handle tab-close mid-stream. The backend never overwrites an existing session ID for a chat row.

A follow-up commit (febe83f) fixes the snapshot-to-frontend translation layer: snapshotMessagesToMikeMessages now handles Strands' nested content block format (both text and textBlock types, and blocks with no type at all), restoring tool activity cards on history reload.

So what If you are building on Strands and want tool activity visible on chat history reload, plan on implementing manual S3 read/write from the start. The `S3SessionManager` path is a dead end for this use case. The session ID timing fix (persist on `chat_id` event, not post-stream) is worth copying regardless of agent framework.

View this fork on GitHub →

Spotted something wrong? Or know the PR text has fresher detail than the writeup above?

Commits in this thread

10 commits from dropthejase/louis, oldest first. Source extracted verbatim from the harvested git log.

SHA Subject Author Date
6905873f refactor(frontend): merge streamChat functions, wire runtimeSessionId, write messages to Supabase, hydrate on load Jason Lee 2026-05-07 ↗ GitHub
commit body
Merges streamChat/streamProjectChat into a single streamChat taking prompt
directly. Generates runtimeSessionId per chat (handleNewChat) and loads it
from chats.agentcore_session_id for existing chats. Writes user and
assistant messages to Supabase chat_messages (removed from the agent).
Hydrates message history from Supabase when opening an existing chat.
619f3d1f feat(agent): wire S3SessionManager for conversation persistence Jason Lee 2026-05-07 ↗ GitHub
Add SessionManager with S3Storage to createAgent; when a sessionId is
provided the agent auto-persists and restores full message history to
the sessions bucket after each invocation.
702b9381 feat(backend,frontend,infra): chat history from S3 snapshot; sessions bucket Jason Lee 2026-05-07 ↗ GitHub
commit body
Add GET /chat/:chatId/messages backend route that reads the Strands
snapshot_latest.json from S3 and transforms it into MikeMessage[].
Replace Supabase chat_messages hydration in useAssistantChat with a
call to this endpoint; remove frontend chat_messages writes. Add
sessionsBucket to StorageStack and wire SESSIONS_BUCKET_NAME env var
into ApiStack Lambda.
e63a501b feat: AgentCore user ID header, session ID format, Cognito-compatible schema Jason Lee 2026-05-08 ↗ GitHub
40c1e3ad feat: persist AgentCore session ID for multi-turn continuity across refreshes Jason Lee 2026-05-08 ↗ GitHub
commit body
- Add agentcore_session_id + agentcore_session_created_at columns to chats schema
- Add PUT /chat/:chatId/session-id endpoint - writes on first turn, no-ops if already set
- Frontend saves runtimeSessionId to DB after each turn (fire-and-forget)
- On chat load, GET /chat/:chatId/session-id restores the session ID so
  AgentCore can resume the correct S3 snapshot after page refresh
af04dc99 fix: persist AgentCore session ID on chat_id SSE event, not post-stream Jason Lee 2026-05-08 ↗ GitHub
Previously saved after stream completed - tab-close mid-stream lost the
session ID. Now fires immediately when the chat_id event arrives (first
SSE event, before AgentCore runs) so any interruption still persists it.
ae70567f fix(chat): load messages from S3 snapshot instead of empty chat_messages table Jason Lee 2026-05-12 ↗ GitHub
commit body
- getChat() now calls GET /chat/:chatId and GET /chat/:chatId/messages in parallel; messages come from S3 AgentCore session snapshot
- Fix snapshotMessagesToMikeMessages to accept content blocks with no type field (Strands snapshot omits type on text blocks)
- Update FEATURE-PARITY.md with pending items: tier display, account settings parity, credit enforcement
efdae23d feat(agents,api): replace SessionManager with manual S3 conversation persistence Jason Lee 2026-05-13 ↗ GitHub
commit body
Drop Strands SessionManager/S3Storage (baked stale system prompt into every
turn). Own S3 read/write via AfterInvocationEvent hook on conversations/{chatId}/messages.json.
System prompt always fresh from code. API chat history reads same key directly.
838d29ba Merge feature/session-persistence-without-s3sessionamanger into development Jason Lee 2026-05-13 ↗ GitHub
febe83f5 fix(api): restore tool activity + reasoning blocks on chat history reload Jason Lee 2026-05-13 ↗ GitHub
commit body
snapshotMessagesToMikeMessages now handles nested Strands snapshot format:
- toolUse/toolResult blocks use nested objects (no type field) not flat Bedrock format
- reasoning blocks parsed and emitted as reasoning events
- text block filters exclude nested blocks to avoid false matches
- read_document filename extracted from document_metadata block in result text

Capture this thread into my fork

Download a single Markdown prompt that tells Claude how to port every commit above into your working tree — adapting paths and structure to match your repo. Run it via claude -p < capture-thread-334.md from inside the repo you want the changes in.

⬇ Download capture-thread-334.md