Skip to main content

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 to ctx_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 to ctx_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_idx or create a phantom ctx_turns row.
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)

The raw_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. Migration ALTER 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 treat NULL trigger_source as 'user' via IS NULL coalescing.
  • The message column 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_idx is a 1-based count of user turns. getUserTurnCount (the counter used when writing a new ctx_turns row) applies the same trigger_source = 'user' OR IS NULL filter so automated turns do not inflate turn_idx or create phantom rows.
Related: Session Analytics · Heartbeat · Gateway Configuration · Session Management