Session History Hygiene — Clean Replay and Turn Provenance
WednesdayAI separates what is stored from what the model sees in context. Channel connectors inject metadata (sender id, conversation label, group context) into each user message as a JSON framing block so the agent knows who sent it. Storing and replaying that framing verbatim wastes tokens, inflates the context window, and can confuse models that see “[Conversation info …]” blocks in every prior turn. Session history hygiene addresses this at the storage layer.What changes
Clean message column (SC1)
When a user turn is written toctx_session_entries, the storage layer splits it into two representations:
On context replay (hydration), WednesdayAI feeds the model the clean
message column by default. The raw framing is available for diagnostics and replay in framed mode (see Replay mode below).
This applies to both plain-string content (string role messages) and structured array content (content-block arrays that include a text block containing the framing header).
Turn provenance columns (SC3)
Every row written toctx_session_entries gains four new columns that record where the turn came from:
trigger_source is derived from the session key format (storage-normalize.ts:deriveTriggerSource):
A partial index
idx_ctx_session_entries_trigger_source on ctx_session_entries(trigger_source) WHERE trigger_source IS NOT NULL supports efficient filtering by provenance.
Automated turns filtered from recall and analytics (SC2)
Two production queries that drive context replay and analytics now exclude automated turns:
This means:
- Session recall (
readByKey) on a heartbeat session key returns empty — heartbeat turns are not folded back into conversation context. - Analytics (last user turn, turn counts) count only human-initiated turns. A heartbeat that fires while no user has messaged does not increment
turn_idxor create a phantomctx_turnsrow.
IS NULL in the filter preserves backward compatibility: rows written before this schema version have trigger_source = NULL and are treated as user turns (same as 'user').
Raw entry preserved (SC4)
Theraw_entry (and raw_line) column always stores the full original framing-intact entry exactly as produced by the connector. Stripping happens only on the message column. Diagnostics, audit trails, and framed replay read raw_entry.message directly.
SQLite and Postgres parity (SC5)
The schema additions and filter changes are identical on both backends. MigrationALTER TABLE … ADD COLUMN IF NOT EXISTS runs at startup on existing databases — no manual migration step is required.
Replay mode (SC6)
agents.defaults.ctx.replayMode controls which representation the model receives on context hydration:
Set in
openclaw.json:
"framed" is available for installations where downstream tooling or custom persona prompts depend on the channel framing block being present in history. New installations should use the default "clean".
Example queries
Filter to user-only turns
Count automated turns in a session
Identify sessions with recent automated activity
Backward compatibility
- Rows written before this schema version have
trigger_source = NULL,is_automated = NULL,sender_display = NULL,sender_id_raw = NULL. The production recall and analytics queries treatNULLtrigger_source as'user'viaIS NULLcoalescing. - The
messagecolumn for legacy rows may contain framing if it was written before the strip logic existed.replayMode: "framed"is the safe choice for sessions spanning the migration boundary if framing consistency matters for a persona. ctx_turns.turn_idxis a 1-based count of user turns.getUserTurnCount(the counter used when writing a newctx_turnsrow) applies the sametrigger_source = 'user' OR IS NULLfilter so automated turns do not inflateturn_idxor create phantom rows.