docs/features/chat/retention.md

Chat Retention and Transcripts

Conversation data lives in three stores with three different lifetimes. Nothing is a copy of anything else, so "how long is a chat kept?" depends on which of them you mean.

StoreHoldsLifetime
PortfolioStack-AdminDatachat_started / chat_turn / chat_error90 days raw, 400 days rolled up
PortfolioStack-ChatSessionsResume cursor, session id, first message, turnsCHAT_RETENTION_DAYS (15)
PortfolioStack-EveWorldRuns, events, and the streams that hold the textEVE_WORLD_RETENTION_DAYS (15, must match)
chat/exports/ (S3)Debug exports saved from the chatbotCHAT_EXPORT_RETENTION_DAYS (7)
chat/exports/saved/ (S3)Conversations kept from the admin UIForever — pruning skips this prefix

pnpm check:retention asserts the session TTL and the eve run-log TTL still agree; a shorter run-log window would resume a live session into deleted events.

Reading a conversation back

The transcript is not in the portfolio's own tables — only the agent Lambda can read the eve world table. src/server/chat-eve/transcript.ts therefore replays the conversation through the agent's own stream endpoint (/eve/v1/session/<sessionId>/stream?startIndex=0) with the same channel JWT the live bridge mints, and reduces the event stream to messages: message.received for the visitor, message.completed for the agent.

Two things make a replay terminate:

  • session.waiting is not terminal. It fires between turns, so stopping there truncates every conversation to its first question. Only session.completed and session.failed end a session.
  • The event budget does. A parked session keeps its stream open for a turn that never comes, so the replay stops once it has seen the session record's stored streamIndex events, with a 15s abort as the backstop.

pnpm check:transcript asserts the event-to-message reduction and the saved Markdown shape.

Inspecting production sessions from the CLI

pnpm chat:logs replays the newest retained session and prints the useful Eve events: completed reasoning, messages, tool calls and results, token usage, and terminal state. It drops token-by-token deltas so the causal path stays readable.

pnpm chat:logs
pnpm chat:logs -- --list
pnpm chat:logs -- wrun_...
pnpm --silent chat:logs -- --json
pnpm chat:logs -- wrun_... --full

The command is read-only. It gets the session id and replay event budget from PortfolioStack-ChatSessions, discovers the deployed agent URL from the PortfolioStack CloudFormation output, and reads the durable stream through the agent's authenticated HTTP API. Tool results are capped in the default view; --full preserves them when their omitted tail matters.

Admin surface

/admin/chat-exports (nav: "chats") lists every session still inside the retention window, newest first, and expands each one into its transcript. Save re-renders the conversation as Markdown into chat/exports/saved/, which retention pruning skips — that is the only way a conversation outlives its 15-day TTL. Sessions that started before the title was recorded list under their session id; the title is written on each turn from the visitor's first message.

A new route under /api/admin/ needs an entry in the admin function's routes allow-list in open-next.config.ts. The traffic pattern (api/admin*) sends the request to that function, but the bundle is built from the list, so a missing entry deploys as a 500 with no handler module. pnpm check:routes asserts the two agree and gates the deploy.

Engagement counts (chat_started, chat_turn, chat_error) are beacon events, not session data. They appear in the Events panel at /admin/analytics.