Skip to content

feat(memory): Background memory-review daemon — async post-turn reflection without blocking the user #3024

@hamza-jeddad

Description

@hamza-jeddad

Background

Sub-issue of #3011.

The agent currently saves memories only when the model explicitly calls a memory tool (pkg/tools/builtin/memory/) mid-conversation. Many memory-worthy moments are missed: the user corrects the agent's behaviour, reveals a preference implicitly, or asks a question that implies a stable environmental fact — none of these trigger an explicit tool call.

A background review daemon solves this: after every N turns, a lightweight review process runs asynchronously after the response is delivered. It examines the recent conversation and decides whether anything is worth persisting, without competing with the user's next message for model attention or token budget.

This forks a constrained mini-agent with only memory-write tools allowed, runs it against a snapshot of the conversation, and lets its writes land directly on the shared memory store.

Proposed design

1. Trigger: configurable nudge interval

Add four config fields to AgentConfig in pkg/config/latest/types.go:

YAML key Go type Default Description
memory_review_interval *int 0 (disabled) Run background review every N user turns. 0 = disabled.
memory_review_model string "" (inherit main) Model to use for review. Empty = same model as main agent.
memory_review_timeout *int 30 Hard timeout in seconds for the review goroutine.
memory_review_verbose *bool false If true, surface a compact summary to the user after writes.

The runtime in pkg/runtime/ tracks turnsSinceReview int. At the end of every turn, if turnsSinceReview >= memory_review_interval and memory_review_interval > 0, a review is scheduled and turnsSinceReview is reset to 0.

2. Review goroutine

The review runs in a detached goroutine:

go runMemoryReview(ctx, reviewCfg, messagesSnapshot, memStore)
  • messagesSnapshot is a deep copy of the conversation at the moment the review is triggered — the main session continues independently.
  • The goroutine has a hard timeout (memory_review_timeout, default 30 s).
  • Writes go directly to the shared memory DB via the same add_memory / update_memory paths, so security scanning (sibling sub-issue B) and budget enforcement (sibling sub-issue A) apply automatically.
  • The goroutine never calls any tool other than add_memory, update_memory, and delete_memory — a runtime allow-list enforces this.

3. Review prompt

The review goroutine sends the conversation snapshot to the model with the following review prompt (stored alongside other prompts in the repo, e.g. pkg/memory/review/prompts/):

Review the conversation above and consider saving to memory if appropriate.

Focus on:
1. Has the user revealed things about themselves — persona, preferences,
   communication style, pet peeves, or personal details worth remembering?
   → use target="user"
2. Has the agent learned something about the environment, project conventions,
   tool behaviour, or workflow that would be useful in future sessions?
   → use target="memory"

If something stands out, save it using add_memory.
If nothing is worth saving, respond with "Nothing to save." and stop.
Priority: user preferences and corrections > environment facts > procedural notes.
Do NOT save task progress, completed-work logs, or temporary state.

4. Preventing review-prompt contamination

The review goroutine must not persist the harness prompt itself as a memory entry. This is enforced by:

  • Setting a write-origin tag (write_origin = "background_review") on the goroutine's DB connection.
  • The security scan (sibling sub-issue B) already rejects obvious injection attempts.
  • A max-entries-per-review cap (default: 5) prevents runaway writes.

5. User-visible feedback (optional)

When one or more entries are written and memory_review_verbose: true, emit a compact summary through the existing TUI / event bus — e.g. "Saved 2 memories from your last conversation." This integrates with pkg/tui/components/notification/ or the equivalent event stream.

6. Turn-counter persistence across restarts

In gateway / stateless-server mode (pkg/chatserver/, pkg/gateway/), turnsSinceReview is stored in the session DB (pkg/session/) so the counter survives restarts:

UPDATE sessions SET turns_since_memory_review = ? WHERE id = ?;

On session load, read the stored counter to resume from where the last session left off.

Implementation checklist

  • pkg/config/latest/types.go — add MemoryReviewInterval, MemoryReviewModel, MemoryReviewTimeout, MemoryReviewVerbose to AgentConfig
  • agent-schema.json — extend schema; add descriptions
  • pkg/agent/agent.go — accessors for the four new fields
  • pkg/runtime/turnsSinceReview counter; trigger logic at end of turn
  • pkg/memory/review/review.goRunReview(ctx, cfg, snapshot, store); build review prompt; call LLM with allow-list tool set; write results to store; hard timeout; max-entries-per-review cap
  • pkg/memory/review/ — allow-list enforcer: reject any tool call other than add_memory, update_memory, delete_memory
  • pkg/session/ — persist turns_since_memory_review; reload on session resume
  • memory_review_verbose path: surface compact summary to user after writes via existing notification/event mechanism
  • Unit tests: trigger fires at correct interval; goroutine is detached (main turn does not block on it); allow-list rejects non-memory tools; max-cap prevents > 5 writes per review; timeout cancels stuck review
  • Integration test: run 10-turn session with memory_review_interval: 3; assert at least one background write occurs; assert main-turn latency unaffected (< 5% increase)
  • go test -race passes on pkg/memory/review/

Acceptance criteria

  • With memory_review_interval: N, a review goroutine is launched after every Nth user turn
  • The review goroutine operates on a snapshot copy — the main conversation is never blocked or mutated
  • The goroutine is cancelled after memory_review_timeout seconds with no partial writes left behind
  • Only add_memory, update_memory, delete_memory are callable from within the review; any other tool call is silently dropped
  • No more than 5 entries are written per review cycle
  • Security scanning (sub-issue B) and budget enforcement (sub-issue A) apply to review-originated writes
  • memory_review_interval: 0 (default) produces zero behavioural change
  • Turn counter persists across restarts in gateway mode
  • go test -race passes

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