Files
viettranx 2731f99ad5 feat(memory): context-aware recall query for auto-inject
Auto-inject previously searched episodic memory using only the latest
user message. Follow-up questions like "what's my favorite?" returned
poor matches because the embedding lost the conversational frame.

InjectParams now carries an optional RecentContext field that pgAuto
Injector prepends to the search query as "Context: ... \nQuery: ..."
before running the FTS+vector hybrid search. The "Context:"/"Query:"
framing works with both instruction-tuned embedding models (which
respect the labels) and plain models (neutral separators).

ContextStage walks the message history backward, collects up to 2
trailing user turns capped at 300 runes total, and threads the snippet
through the AutoInject callback to the injector. Empty RecentContext
preserves legacy single-message search semantics — zero-risk fallback
for callers that haven't adopted the new field.

Rune-based truncation (not byte) keeps vi/zh locales safe: a byte-wise
tail-clip would slice multi-byte runes and emit invalid UTF-8 to the
embedding model, degrading exactly the cases Phase 9 is meant to fix.
tailClipRunes helper covers Vietnamese, Chinese, Japanese, emoji.

13 regression tests: recall query builder (unicode-safe clip,
whitespace handling, position ordering), buildRecentContext (order
preservation, turn cap, truncation, non-user skip), and
tailClipRunes (CJK, short input, zero cap). All passing with -race.

Refs plans/260410-1009-openclaw-ts-feature-port/phase-09-active-
memory-recall.md — minimal-viable delivery; Tier 2 LLM re-ranking
and per-session recall cache deferred until operational data shows
context-aware search alone is insufficient.
2026-04-10 12:01:00 +07:00

74 lines
2.7 KiB
Go

// Package memory extends the v3 memory system with auto-injection and tiered retrieval.
//
// V3 design: Phase 3 — L0/L1/L2 context tiering + smart auto-inject.
package memory
import "context"
// AutoInjector checks relevance and produces L0 injection for system prompt.
// Called once per turn in ContextStage.
type AutoInjector interface {
// Inject checks user message against memory index.
// Returns formatted section for system prompt, or "" if nothing relevant.
// Budget: max ~200 tokens of L0 summaries.
Inject(ctx context.Context, params InjectParams) (*InjectResult, error)
}
// InjectParams configures a single auto-inject call.
type InjectParams struct {
AgentID string
UserID string
TenantID string
UserMessage string
// RecentContext carries a short snippet of recent conversation (typically
// the last 1-2 user turns concatenated) used to enrich the search query.
// Context-aware recall: without this, vector search on "what's my favorite?"
// misses memories about the topic under discussion. With it, the query
// embedding captures conversational intent and returns materially better
// matches for follow-up questions.
//
// Empty = legacy behaviour (search on UserMessage only).
// Target length: ≤ ~400 chars. Longer context dilutes the embedding.
RecentContext string
MaxEntries int // default 5
MaxTokens int // default 200
Threshold float64 // relevance threshold (default 0.3)
}
// InjectResult contains the injection output + observability data.
type InjectResult struct {
Section string // formatted prompt section (empty = nothing relevant)
MatchCount int // total matches found
Injected int // entries injected (after budget trim)
TopScore float64 // highest relevance score
}
// L0Summary is a single auto-inject entry for the system prompt.
type L0Summary struct {
Topic string // short topic label
Summary string // ~1 sentence abstract
ID string // for memory_expand(id) deep retrieval
}
// MemoryConfig holds per-agent memory settings (stored in agents.settings JSONB).
type MemoryConfig struct {
AutoInjectEnabled bool `json:"auto_inject_enabled"` // default true
AutoInjectThreshold float64 `json:"auto_inject_threshold"` // default 0.3
AutoInjectMaxTokens int `json:"auto_inject_max_tokens"` // default 200
EpisodicTTLDays int `json:"episodic_ttl_days"` // default 90
ConsolidationEnabled bool `json:"consolidation_enabled"` // default true
}
// DefaultMemoryConfig returns sensible defaults.
func DefaultMemoryConfig() MemoryConfig {
return MemoryConfig{
AutoInjectEnabled: true,
AutoInjectThreshold: 0.3,
AutoInjectMaxTokens: 200,
EpisodicTTLDays: 90,
ConsolidationEnabled: true,
}
}