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.
| Store | Holds | Lifetime |
|---|---|---|
PortfolioStack-AdminData | chat_started / chat_turn / chat_error | 90 days raw, 400 days rolled up |
PortfolioStack-ChatSessions | Resume cursor, session id, first message, turns | CHAT_RETENTION_DAYS (15) |
PortfolioStack-EveWorld | Runs, events, and the streams that hold the text | EVE_WORLD_RETENTION_DAYS (15, must match) |
chat/exports/ (S3) | Debug exports saved from the chatbot | CHAT_EXPORT_RETENTION_DAYS (7) |
chat/exports/saved/ (S3) | Conversations kept from the admin UI | Forever — 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.waitingis not terminal. It fires between turns, so stopping there truncates every conversation to its first question. Onlysession.completedandsession.failedend 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
streamIndexevents, 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.
