Background
All memory currently lands in a single flat SQLite table (pkg/memory/database/). A production memory subsystem distinguishes between two fundamentally different kinds of persistent knowledge:
- Agent notes (
memory target): facts the agent learns about the environment — project conventions, tool quirks, OS details, coding styles, lessons learned.
- User profile (
user target): facts the agent learns about the person — name, role, timezone, communication style, pet peeves, preferences.
Keeping these separate gives operators independent control (e.g. enable user profiling but disable agent notes), gives the model clearer context in the system prompt, and lets each tier have its own size budget.
Sub-issue of #3011.
Proposed design
1. Storage
Add a tier column (TEXT NOT NULL DEFAULT 'memory', values 'memory' or 'user') to the existing user_memories table in pkg/memory/database/sqlite/. Migration required. All existing rows default to 'memory'.
ALTER TABLE user_memories ADD COLUMN tier TEXT NOT NULL DEFAULT 'memory';
CREATE INDEX IF NOT EXISTS idx_user_memories_tier ON user_memories(tier);
2. Memory tool target parameter
Extend add_memory, update_memory, delete_memory, search_memories, and get_memories in pkg/tools/builtin/memory/ with an optional target parameter:
{
"name": "target",
"type": "string",
"enum": ["memory", "user"],
"description": "Which memory tier: 'memory' for agent notes, 'user' for user profile.",
"default": "memory"
}
All existing callers that omit target get 'memory' — fully backward compatible.
3. Per-tier character budgets
Add two config fields to AgentConfig in pkg/config/latest/types.go:
| YAML key |
Go type |
Default |
Description |
memory_char_limit |
*int |
2200 |
Max chars for agent-notes tier |
user_char_limit |
*int |
1375 |
Max chars for user-profile tier |
Enforcement: when add_memory / update_memory would push a tier over budget, return a structured error listing current usage and asking the model to replace or remove an entry first.
4. System-prompt injection (two separate blocks)
The inject_memories builtin (from #3015) must emit two distinct blocks when both tiers are enabled:
════════════════════════════════════════════════
MEMORY (agent notes) [42% — 924/2,200 chars]
════════════════════════════════════════════════
<entry 1>
§
<entry 2>
════════════════════════════════════════════════
USER PROFILE (who the user is) [31% — 427/1,375 chars]
════════════════════════════════════════════════
<entry 1>
§
<entry 2>
Each block is omitted entirely when its tier is empty.
5. Per-agent config toggles
agents:
my_agent:
tools:
- type: memory
memory_enabled: true # agent-notes tier (default: true)
user_profile_enabled: true # user-profile tier (default: true)
memory_char_limit: 2200
user_char_limit: 1375
Implementation checklist
Acceptance criteria
Background
All memory currently lands in a single flat SQLite table (
pkg/memory/database/). A production memory subsystem distinguishes between two fundamentally different kinds of persistent knowledge:memorytarget): facts the agent learns about the environment — project conventions, tool quirks, OS details, coding styles, lessons learned.usertarget): facts the agent learns about the person — name, role, timezone, communication style, pet peeves, preferences.Keeping these separate gives operators independent control (e.g. enable user profiling but disable agent notes), gives the model clearer context in the system prompt, and lets each tier have its own size budget.
Sub-issue of #3011.
Proposed design
1. Storage
Add a
tiercolumn (TEXT NOT NULL DEFAULT 'memory', values'memory'or'user') to the existinguser_memoriestable inpkg/memory/database/sqlite/. Migration required. All existing rows default to'memory'.2. Memory tool
targetparameterExtend
add_memory,update_memory,delete_memory,search_memories, andget_memoriesinpkg/tools/builtin/memory/with an optionaltargetparameter:{ "name": "target", "type": "string", "enum": ["memory", "user"], "description": "Which memory tier: 'memory' for agent notes, 'user' for user profile.", "default": "memory" }All existing callers that omit
targetget'memory'— fully backward compatible.3. Per-tier character budgets
Add two config fields to
AgentConfiginpkg/config/latest/types.go:memory_char_limit*int2200user_char_limit*int1375Enforcement: when
add_memory/update_memorywould push a tier over budget, return a structured error listing current usage and asking the model to replace or remove an entry first.4. System-prompt injection (two separate blocks)
The
inject_memoriesbuiltin (from #3015) must emit two distinct blocks when both tiers are enabled:Each block is omitted entirely when its tier is empty.
5. Per-agent config toggles
Implementation checklist
pkg/memory/database/sqlite/— addtiercolumn + migration; update all query helpers to accept and filter bytierpkg/tools/builtin/memory/— addtargetparam to all five memory tools; enforce per-tier char budget on writes; return structuredusagein every tool responsepkg/config/latest/types.go— addMemoryEnabled,UserProfileEnabled,MemoryCharLimit,UserCharLimittoAgentConfigagent-schema.json— extend schema with new fields + descriptionspkg/hooks/builtins/inject_memories.go— split injected block into two sections; respect per-tier enable flagspkg/agent/agent.go— add accessors for the four new config fieldstier = 'memory'; no data lossAcceptance criteria
add_memory(content="…", target="memory")stores in agent-notes tieradd_memory(content="…", target="user")stores in user-profile tieradd_memory(content="…")(notarget) defaults to'memory'— backward compatiblesearch_memories(query="…", target="user")returns only user-profile entriesget_memories(target="memory")returns only agent-note entrieschar_limitreturns a structured error with current usage and does not truncate existing entriesmemory_enabled: falsesuppresses the agent-notes block;user_profile_enabled: falsesuppresses the user-profile blocktargetusage see zero behavioural change