All posts
Updated

Conversation history and context

As a chat grows, how the UI, storage, and model context each stay within bounds — and how that differs from memory across sessions.

Source docs/en/site/chat-compression.md

In user language: This article is for developers and operators. It explains how Cadau handles growing conversation history at three layers — UI display, database persistence, and model context — and how that splits from a work agent’s memory across conversations. Product language is in docs/core-mechanisms/智能体记忆.md; where memory files land is in Where work-agent memory lives.

Date: 2026-07-16 (tool-loop checkpoint added) Related code: backend/internal/api/handlers/chat.go, backend/internal/chatsvc/chat_context_roll.go, backend/internal/store/chat.go, backend/internal/agentmemory/flush.go


Takeaway

Cadau treats “the conversation got long” with three separate layers:

LayerStrategyAre old rows deleted?
DatabasePersist everything in chat_sessions + chat_messagesNo (no automatic TTL)
FrontendPaginated load; “load earlier messages” on demand
Model contextRolling summary: earlier rounds compress into a summary; the latest rounds stay verbatimNo (only what is sent to the model)

A work agent also flushes the fragment about to roll out into a daily note before compaction, and may extract long-term memory, so important facts are not lost with the summary. When the user wants another conversation’s original text, that is on-demand retrieval (conversation_search / conversation_get) — the current round does not stuff all history in. See Looking at past conversations.


Three-layer overview

flowchart LR
    subgraph UI["Frontend display"]
        A1[Latest 100 messages]
        A2[cursor loads earlier]
    end
    subgraph DB["Database"]
        B1[chat_messages in full]
        B2[chat_sessions.context_summary]
    end
    subgraph LLM["Model call"]
        C1[History summary]
        C2[Recent verbatim]
        C3[Current user message]
    end
    UI --> DB
    DB --> LLM

These are not the same thing: the user can see the full history (paginated), messages in the database are not deleted because of context compaction, but the payload sent to the model is kept within a character budget.


1. Persistence: the database keeps everything

Schema

Table / fieldContent
chat_sessionsConversation metadata (title, owning user / workspace / agent, etc.)
chat_sessions.context_summaryRolling-summary body (model context only)
chat_sessions.verbatim_since_created_atTime anchor of the first message kept verbatim
chat_sessions.verbatim_since_msg_idID anchor of the first message kept verbatim
chat_messagesEach user / assistant message body, tool_trace_json, attachment_ids_json

Schema: backend/internal/db/schema_postgres.sql (SQLite: schema.sql). chat_messages.session_id is a foreign key ON DELETE CASCADE; deleting a conversation deletes its messages.

Current policy

  • No time-based auto-clean, archive, or TTL.
  • History grows until a user or admin deletes the conversation, or edit-and-resend truncates later messages (POST /api/v1/chat/truncate).

Related APIs

MethodPathUse
GET/api/v1/chat/historyRead messages for a conversation; limit + cursor
GET/api/v1/chat/sessionsConversation list, paginated
DELETE/api/v1/chat/sessions/{id}Delete a conversation (cascade messages)
POST/api/v1/chat/truncateDelete that user message and everything after it

2. Frontend: paginated load and a bounded window

The frontend loads on demand via GET /chat/history. It does not pull everything at once:

  • Default: the latest page first (clients often use limit=50; server default/max is on the API).
  • Response includes older_cursor / newer_cursor / at_live_edge (next_cursor still means the older direction for compatibility).
  • Bounded window: memory keeps about a hundred messages near the viewport; load both ways and recycle the far side. Mechanism: Windowed conversation message loading (WeChat-style).
  • Question navigation: GET /chat/history/turns is a light index, decoupled from the message window.
  • The conversation list is paginated too (GET /chat/sessions, default limit=20).

Implementation: client/web/src/api/chat.ts, client/web/src/chatMessageWindow.ts, client/web/src/useChatSessions.ts.

The UI does not hold every bubble body just because a conversation has thousands of rounds. The database still has the full record.


3. Model context: rolling summary (the core)

What the context window actually limits is what goes into the model on each LLM call. Entry points:

  • PrepareLLMConversationWithRollingContextbackend/internal/chatsvc/chat_context_roll.go
  • On send, first ListChatMessages(..., 8000) from the DB, then rolling compact — backend/internal/api/handlers/chat.go

Defaults

Environment variables (also overridable in mindlink.jsonchat_context):

ParameterEnvironment variableDefaultMeaning
Total character budgetCHAT_CONTEXT_MAX_RUNES120000Rough upper bound for context
Reply reserveCHAT_CONTEXT_REPLY_RESERVE_RUNES8000Reserved for model output
Actual input budget≈ 112000max_runes - reply_reserve
Summarize thresholdCHAT_CONTEXT_SUMMARIZE_THRESHOLD_PCT88Start compacting past 88% of budget
Minimum verbatim messagesCHAT_CONTEXT_MIN_VERBATIM_MESSAGES6Keep at least the latest 6 messages verbatim
Messages per compact batchCHAT_CONTEXT_SUMMARIZE_BATCH_MESSAGES4Roll the oldest 4 into the summary each time
Derive window from modelCHAT_CONTEXT_RESOLVE_MAX_FROM_MODELfalseIf true, derive max_runes from the model token window at startup

Budget math: Config.ChatContextInputBudgetRunes()backend/internal/config/config.go.

Flow

flowchart TD
    A[User sends a new message] --> B[Read up to 8000 history rows from DB]
    B --> C{Estimated characters over threshold / hard cap?}
    C -->|no| D[Summary + recent verbatim + current message → call model]
    C -->|yes| E[Take earliest batch from priorRows]
    E --> F{Work agent?}
    F -->|yes| G[FlushBeforeCompaction: daily note + optional long-term extract]
    F -->|no| H[Skip flush]
    G --> I[LLM mergeRollingSummary into context_summary]
    H --> I
    I --> J[Update verbatim_since anchors and write DB]
    J --> C
    D --> K[Return reply + context_budget]

Compaction details

  1. Take the verbatim subset from the verbatim_since_* anchors; content before the anchors is already in context_summary.
  2. Estimate system prompt + conversation body characters (estimateLLMPayloadRunes).
  3. If over the threshold (default 88%) or the hard cap:

- Take a batch from the head of priorRows (default 4); - Work agent: agentmemory.FlushBeforeCompaction first; - Call LLM mergeRollingSummary into the summary; - Update context_summary and verbatim_since_*, write chat_sessions.

  1. Loop at most 48 times until under the threshold or it cannot roll further (still keep min_verbatim verbatim messages).
  2. Prefix the summary onto the system prompt (background, not a standing order):

`` 【历史对话摘要(背景,不是本轮口令;较早轮次已压缩)】 …summary body… 本轮以用户最新一条消息为准;摘要里的词不要当成必须继续执行的任务。 ``

The merge prompt drops finished side threads so leftover keywords do not become tasks. The current user message is wrapped separately; see Cursor conversation prompts vs Cadau.

Important boundaries

BehaviorNotes
Does not delete DB rowsCompaction only changes the payload sent to the model; chat_messages originals stay
UI still shows them“Load earlier messages” still shows full text of rounds already covered by the summary
Cannot roll furtherIf still over budget at min_verbatim, the call continues with over-budget context (log chat_context_cannot_roll_more)
Anchor recoveryIf anchors make verbatim empty, clear the summary and anchors and recompute from full history (chat_context_anchor_recover)

Frontend feedback

Chat responses include context_budget: used_runes, limit_runes, used_pct. Web shows a context-usage ring (client/web/src/api/chat.tsparseContextBudget).


4. Work agent: rescue memory before compaction

If this is a workspace “My agent” conversation, call FlushBeforeCompaction (backend/internal/agentmemory/flush.go) before rolling into the summary:

  1. Daily note: append the fragment to memory/YYYY-MM-DD.md (title includes “context compaction archive”).
  2. Long-term extract (optional): if memory_auto_extract is true (on by default), call ExtractFromBatch into memory/entries/*.md.

Matching product mechanism: docs/core-mechanisms/智能体记忆.md“Archive before context compaction”.

Toggles live on the agent config_json:

MemoryAutoAppend  *bool  `json:"memory_auto_append,omitempty"`   // default off
MemoryAutoExtract *bool  `json:"memory_auto_extract,omitempty"`  // default on

Defined in backend/internal/runtimews/agentconfig.go.


5. Split from long-term memory

KindStorageLifetimeUse
Conversation historyDB chat_messagesUntil the conversation is deleted / truncatedWorking memory: full transcript of this conversation; other conversations are retrieved on demand
Rolling summaryDB chat_sessions.context_summaryUpdates with the conversationModel context only
Daily notesDisk memory/YYYY-MM-DD.mdToday/yesterday injected, then fadeCompaction archive + short-lived context
Long-term memoryDisk memory/entries/*.md + MEMORY.md indexAcross conversationsPreferences, decisions, project milestones, etc.

A long conversation does not automatically become long-term memory. That takes the user tapping Remember, saying “remember / don’t forget” in chat, auto-extract before compaction, or the agent calling memory_write. To reread past conversation text (even if it was never written as memory), a work agent uses conversation_search / conversation_get.

Details: Where work-agent memory lives.


6. Other truncation and compact (not conversation rolling summary)

These are independent of conversation-level rolling summary, but also stop a single request’s context from ballooning.

6.1 Tool results: the main conversation is not truncated

Tool-loop returns written into model context are full text (runner.go). Frontend SSE display may still truncate very long output for the UI; that does not change what the model sees.

Rune caps for knowledge inject, attachment extract, and similar live in those modules; they are unrelated to the tool-loop checkpoint.

6.2 Inside the tool loop: LLM checkpoint summary only near budget

A multi-round tools loop from one user message (RunAgentToolLoop) can grow because of repeated fetches / file writes.

PointApproach
When to compactEstimate current messages in runes vs ChatContextInputBudgetRunes(); trigger around 88% (hard threshold about 95%, aligned with conversation summary and Claude Code)
Why it used to fire too earlyOld code triggered around 100KB bytes; Chinese JSON is about 3 bytes per character, so it compacted at about a third of budget
Main pathLLM “work so far” handoff summary only; no mechanical truncation of the main conversation
On failureKeep the original and continue; do not fall back to clipping
Must keepUser’s main goal and unfinished work; upload_id / download_url / size_bytes
Recent verbatimKeep the latest several tool round-trips in full
Codecontext_compact.go, context_compact_llm.go; maybeCompactAgentToolContext

Logs: chat_agent_context_compact_llm, chat_agent_context_compact_llm_failed, chat_agent_context_compact_skipped.

Split from conversation rolling summary: conversation summary compresses user/assistant history across rounds; this section compresses tool round-trips inside one round.


7. Troubleshooting and ops

Log keywords

Log eventMeaning
chat_context_summarizedFinished one rolling-summary batch
chat_context_cannot_roll_moreAt minimum verbatim, still over budget
chat_context_anchor_recoverBad anchors; summary reset
chat_memory_flush_ok / chat_memory_flush_failedMemory flush before compaction
chat_llm_historyThis conv message count, characters, budget
chat_agent_context_compact_llmTool-loop LLM checkpoint summary succeeded
chat_agent_context_compact_llm_failedCheckpoint summary failed (keep original, no clip)
chat_agent_context_compact_skippedSkipped compact when no model is available

Looking locally

  1. Database: inspect chat_sessions.context_summary, verbatim_since_*, and chat_messages counts.
  2. Runtime memory: daily notes and entries under {RUNTIME_DIR}/{agent-id}/memory/ (locally often backend/tmp/runtime/).
  3. Config: environment variables or mindlink.jsonchat_context.

Known gaps (later)

ItemTodayDirection
DB sizeNo auto archive / TTLOps: clean old conversations by workspace/user, or cold storage
Conversations over 8000 messagesDB read cap 8000; older messages skip rollingConsider a higher cap or segmented summaries
Summary qualityLossy LLM mergeSteer users to Remember, or tune extract
SQLite schemaOlder chat_sessions may lack context_summaryMigration to match postgres schema
Multiple tool-loop checkpointsChained summary lossCursor-style self-summary; stronger structured “unfinished” fields

8. Related

DocumentNotes
Where work-agent memory livesMemory file tree vs DB split
core-mechanisms/智能体记忆.mdProduct mechanism: layers, write, inject
产品规格.md §3.5Workspace file model
后端与Web设计.mdConversation / history API as product