Skip to content

feat(memory): Hook memory into context-compression — extract facts before discard and refresh snapshot after compaction #3023

@hamza-jeddad

Description

@hamza-jeddad

Background

Sub-issue of #3011.

When the conversation grows too long, docker-agent compacts older messages via pkg/compaction/ and pkg/runtime/compactor/. Today the memory subsystem has no awareness of this event, which means:

  1. Facts about to be discarded are lost. Anything the model learned during the summarised portion of the conversation never makes it into the memory store, even if it would have been worth saving.
  2. The frozen snapshot goes stale. If a memory write happened during the compressed portion, the snapshot held by inject_memories (feat(memory): Frozen snapshot + cache invalidation for inject_memories #3017) still reflects pre-write state.
  3. Session scoping breaks. Per-session state keyed by session_id is broken by compaction-driven session rotation; consumers need to be told about the switch.

Proposed design

1. OnPreCompress and OnSessionSwitch hooks on a memory coordinator

Define a new interface in pkg/memory/coordinator.go:

// pkg/memory/coordinator.go
type Coordinator interface {
    // … existing methods …

    // OnPreCompress is called before compaction discards messages.
    // It receives the messages about to be summarised/dropped and
    // returns a hint string for the compressor (may be empty).
    OnPreCompress(ctx context.Context, messages []Message) (hint string, err error)

    // OnSessionSwitch is called whenever the active session ID changes.
    OnSessionSwitch(ctx context.Context, newSessionID, parentSessionID string, reset bool) error
}

Call site in the compactor (pkg/runtime/compactor/):

hint, _ := memCoordinator.OnPreCompress(ctx, messagesToCompress)
// pass hint to compression-summary prompt so the summariser preserves key facts
summary := compressor.Summarise(ctx, messagesToCompress, hint)

2. What OnPreCompress does

Inside the coordinator:

  1. Scan the messages about to be discarded for memory-worthy content using a lightweight extraction heuristic (keyword density, question–answer pairs, explicit user corrections, preference statements).
  2. For each candidate fact, call add_memory (agent-notes tier) or add_memory(target="user") (user-profile tier) as appropriate — subject to the same write-time security scan (sibling sub-issue B) and budget enforcement (sibling sub-issue A).
  3. Return a short string summarising what was extracted (for the compressor prompt in pkg/compaction/prompts/).

Step 2 should be best-effort and non-blocking: if the extraction LLM call times out or fails, log and return an empty hint rather than blocking compaction.

3. OnSessionSwitch — snapshot refresh after session-ID rotation

Compaction typically rotates the session_id. After the new session is created:

memCoordinator.OnSessionSwitch(ctx, newSessionID, oldSessionID, reset=false)

Inside the coordinator, OnSessionSwitch:

4. Snapshot refresh on compaction

After OnSessionSwitch, the next call to inject_memories (#3015) finds a stale (generation-bumped) snapshot and rebuilds it from the DB, now including the facts extracted by OnPreCompress. The user sees the newly extracted memories injected in the very next turn — no session restart required.

5. Signal for compaction events

The compactor must emit a signal the memory coordinator can subscribe to, or the coordinator must be passed to the compactor as a dependency. Use the existing hook/event bus pattern in pkg/hooks/ if applicable; otherwise add a direct call from the compactor.

Implementation checklist

  • pkg/memory/coordinator.go — define Coordinator interface with OnPreCompress and OnSessionSwitch; implement on the concrete coordinator struct
  • OnPreCompress: lightweight extraction heuristic (no LLM call required for MVP — keyword / pattern matching is sufficient); call add_memory for each extracted fact; respect security scan and budget limits; return hint string
  • OnSessionSwitch: invalidate snapshot generation counter; update cached session_id; reset per-session counters when reset=true
  • pkg/runtime/compactor/ call site — call OnPreCompress before discarding messages; call OnSessionSwitch after new session is created
  • pkg/runtime/hooks.go — propagate new session ID to memory coordinator on /reset, /new, /resume, /branch (same triggers as executeStopHooks session rotation)
  • Unit tests: mock OnPreCompress receives the correct subset of messages; facts are inserted via add_memory; OnSessionSwitch invalidates snapshot; reset=true clears per-session state
  • Integration test: run a session that accumulates memories, trigger compaction, assert extracted facts appear in the next turn's injected block

Acceptance criteria

  • OnPreCompress is called before any message is discarded during compaction
  • At least one fact from the compressed messages is extracted and persisted (when the heuristic finds candidates)
  • Extracted facts are subject to the same security scan as user-initiated writes (sub-issue B)
  • OnSessionSwitch is called after session-ID rotation; the next turn's injected block reflects the new state
  • reset=true clears per-session counters; reset=false preserves them
  • OnPreCompress failure never blocks or aborts compaction
  • No regression in compaction latency > 10% on the existing benchmark

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/agentFor work that has to do with the general agent loop/agentic features of the apparea/sessionsFor features/issues/fixes related to session lifecycle (resume, persistence, export)area/toolsFor features/issues/fixes related to the usage of built-in and MCP tools
    No fields configured for Enhancement.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions