This document describes the Extended Memory subsystem for odek. It is a reference-level design: what the module does, how it stores and retrieves memories, the trust model that keeps it safe, and the configuration that controls it.
Extended Memory is opt-in. When disabled, odek keeps the existing three-tier memory system described in MEMORY.md unchanged.
Implementation status: Phases P0–P5 are implemented and active: atom store, dedicated LLM wiring, semantic recall, size-cap eviction, user-state model, quarantine promotion, atom associations, and predictive/proactive surfaces.
- Near-comprehensive recall of the user's own statements, preferences, goals, and recurring patterns.
- Anticipatory context: the agent should load the context for the question the user is about to ask before they ask it.
- Bounded resource usage: a hard 100 MB default disk cap with intelligent eviction.
- Semantic retrieval: configurable top-K vector search over memory atoms instead of session-level summaries.
- Trust-preserving storage: user speech is trusted by default; everything indirect is tainted and quarantined until explicitly approved.
- Cost isolation: memory work can run on a dedicated, lightweight LLM while the main agent keeps using a powerful reasoning model.
The existing memory system stores session episodes: one narrative summary per finished session. That is good for "what did we do last Tuesday" but poor for "the user prefers short answers" or "we always add tests after refactoring auth code."
Extended Memory stores atoms: small, typed, semantically indexed memory objects extracted from the user's own messages. Atoms are the unit of search, ranking, eviction, and provenance. Session episodes remain as an aggregate legacy layer, but atoms become the primary recall surface.
The single most important design decision is the trust boundary.
| Source Class | Default Trust | Recallable Without Promotion |
|---|---|---|
user_said |
Trusted | Yes |
user_approved |
Trusted | Yes |
inferred |
Tainted / quarantined | No |
tool_output |
Tainted | No |
file_read |
Tainted | No |
web |
Tainted | No |
mcp |
Tainted | No |
subagent |
Tainted | No |
agent_generated |
Tainted | No |
User inputs and explicitly user-approved atoms are trusted because they come from the operator. Indirect content may be prompt-injected, malicious, or simply noisy, so it is never auto-recalled. inferred atoms are treated as tainted: they are quarantined until a user explicitly promotes them, matching the code's IsTaintedSourceClass classification. Promotion paths include inline commands such as:
odek, remember thatodek, remember the browser outputodek memory extended promote <atom-id>
Promoted atoms change source class to user_approved and become recallable. Tainted atoms can be removed with odek memory extended forget <atom-id> or age out via quarantine_ttl_days.
The atom is the atomic unit of Extended Memory.
type MemoryAtom struct {
ID string // stable identifier, 128-bit random hex (32 chars)
CreatedAt time.Time // UTC
SourceClass string // user_said | inferred | user_approved | tool_output | file_read | web | mcp | subagent | agent_generated
Type string // preference | intent | fact | decision | goal | convention | file | error | question | observation (legacy)
Text string // the memory itself, capped to atom_max_chars
Context AtomContext // session, project, turn, related atom IDs
Vector vector.Vector // embedding of Text; NOT persisted in JSON, rebuilt from chunk text
Confidence float32 // 0.0 - 1.0
Pin bool // pinned atoms are never evicted
}
type AtomContext struct {
SessionID string `json:"session_id"` // originating session
Turn int `json:"turn"` // turn within the session
Project string `json:"project"` // working directory at creation
RelatedAtomIDs []string `json:"related_atom_ids"` // semantic/temporal/task links
}| Type | Meaning |
|---|---|
preference |
User style choices: verbosity, humor, formality, tone, explanation depth. |
intent |
A goal or direction the user stated or strongly implied. |
fact |
A durable fact the user asserted about themselves or the project. |
decision |
A recorded decision the user made (e.g., "use PostgreSQL over MySQL"). |
goal |
A longer-term objective distinct from a single intent. |
convention |
A recurring pattern or rule the user follows (e.g., "always add tests after auth changes"). |
file |
A file or path the user referenced as important. |
error |
A recurring error or failure mode the user encountered. |
question |
A durable question the user asked that may be worth recalling. |
observation |
Legacy fallback for unknown atom types. Preserved for backward compatibility; new atoms should use one of the nine types above. |
The current extraction prompt and tool schema accept all nine primary types. Unknown types supplied by the LLM are normalized to observation.
~/.odek/memory/
├── facts/ # existing user/env facts
├── episodes/ # existing session summaries
└── extended/
├── atoms.json # atom metadata + provenance
├── chunks/ # one .md file per atom
│ └── <atom-id>.md
├── vectors.gob # persisted go-vector store
├── vectors.gob.emb # persisted embedder state (RP vocabulary / HTTP fingerprint)
├── vectors_meta.json # embedding-space fingerprint for invalidation
├── quarantine.json # tainted atoms awaiting promotion
├── user_model.json # persisted inferred user state + pending review queue
└── associations.json # bidirectional atom association links
All files are written atomically and use 0600 permissions. The vector store and metadata are rebuilt from the chunk files if they are ever corrupted. The user model and associations are loaded at startup and persisted on every change.
Extended Memory can use its own LLM, separate from the main agent. This is ideal because memory work is a stream of small, structured tasks: atom extraction, user-state inference, and intent prediction. A 7B-14B local model is usually sufficient and costs orders of magnitude less than calling a large reasoning model every turn.
If memory.extended.llm is omitted, the module MUST use the default global model. The default global model is the fully resolved main agent LLM after all config layers have been merged: top-level model, base_url, api_key, thinking, max_tokens, temperature, and timeout from ~/.odek/config.json, ODEK_* environment variables, and CLI flags. Extended Memory does not read any of those values again; it reuses the exact llm.Client instance constructed for the main agent loop.
If the default global model has reasoning/thinking enabled, memory extraction and reranking may be expensive. In that case the operator should configure a dedicated memory.extended.llm for cost isolation; a warning is emitted when thinking is enabled and no dedicated memory LLM is configured.
- Per-turn atom extraction: read the latest user message, emit 0-N JSON atoms.
- User-state inference: every
user_state_turn_intervalturns (or when the inferred focus changes), update a persistent user model from recent trusted atoms. - Predictive intent generation: produce up to
predictive_intentslikely follow-up questions before the main agent answers, and recall atoms for those predicted intents. - Optional reranking: rerank semantic-search candidates before they are injected into the system prompt.
The memory LLM is data-bound:
- It only sees user messages and already-trusted atoms.
- It never sees tool output, web content, MCP results, or subagent summaries.
- Its outputs are scanned with
ScanContentbefore persistence. - A compromised memory LLM can only add atoms; tainted atoms are still filtered by provenance.
After every user message, the memory LLM runs a tiny structured extraction.
System prompt:
You are a memory extraction system. Read the user message and extract durable, reusable atomic memories.
For each atom, output an object with:
- "text": a concise, first-person-paraphrased statement (not a command).
- "type": one of "fact", "preference", "intent", "decision", "goal", "convention", "file", "error", "question".
- "confidence": a number 0.0-1.0 indicating how certain the memory is.
Rules:
- Only extract stable information worth recalling in future sessions.
- Do NOT extract instructions, commands, or requests to perform actions.
- Do NOT extract ephemeral details specific only to this message.
- If nothing durable is present, return an empty array.
Output ONLY a JSON array. Example:
[{"text":"User prefers concise answers","type":"preference","confidence":0.9}]
Extracted atoms are immediately:
- Wrapped untrusted content (
<untrusted_content_*>) is stripped from the user message before extraction. - Scanned for injection patterns, invisible Unicode, confusable scripts, and credential patterns (
sk-..., PEM private keys, bearer tokens). If the optional guard is enabled formemory, the same content is also sent to the configured sidecar for a second opinion. - Assigned
source_class: user_said. - Embedded and written to the atom store.
- Checked against the 100 MB size cap; low-retention atoms are evicted if needed.
Extended Memory replaces episode-based recall with semantic search over atom vectors.
- Embed the latest user message via
memory.extended.embedding. - Query the go-vector store for the top
semantic_search_top_k * semantic_search_overfetchcandidates. - Drop tainted atoms and atoms below
semantic_search_min_score. - Compute a composite score:
0.6 * cosine_similarity + 0.4 * retention_score. - Optionally rerank the candidate set with the memory LLM.
- If
follow_up_anticipation_enabledis true, generate predicted intents from the current message, recent messages, and the user model, and union their recall results with the literal-query results (including type-targeted recall forconvention,file, anderroratoms). - Deduplicate, re-rank by retention score, and return the top-K atoms, bounded by
memory_budget_chars.
Candidate atoms are scored by a composite function before injection into the system prompt:
retention_score = confidence
* decay_factor
* trust_boost
decay_factor = 0.5 ^ (age_days / decay_half_life_days)
trust_boost = 1.0 for user_said and user_approved
= 0.0 for inferred and all tainted source classes
composite_score = 0.6 * cosine_similarity(query_vector, atom_vector)
+ 0.4 * retention_score
The final recall result is also bounded by memory_budget_chars. Tainted atoms are excluded regardless of score. When predictive intent recall is enabled, results from predicted intents are deduplicated with the literal-query results and then re-ranked by retention score before the top-K cut.
When follow_up_anticipation_enabled is true (default), the memory LLM receives the current user message, the last several user messages, and the current user-state model. It returns a JSON array of up to predictive_intents likely follow-up intents. Each intent is embedded and searched, and the union of literal-query matches and predicted-intent matches is injected into the main agent's context. For each predicted intent, the system also performs a type-targeted recall for convention, file, and error atoms so the agent can pre-load relevant conventions, references, and known failure modes.
Example:
- User: "Refactor the auth package to remove JWT."
- Predicted intents: "how do I migrate refresh tokens?", "which tests should I update?", "what replaces JWT?"
- Agent's context now includes prior atoms about auth conventions, prior JWT discussions, and the user's preferred test style before the user asks the next question.
Predicted intents are generated by the dedicated memory LLM when configured; otherwise the main LLM is used. A generation failure falls back to literal-query recall only.
The user model is a live, evolving JSON document stored in extended/user_model.json.
{
"style": {
"verbosity": "low",
"humor": "dry",
"formality": "casual",
"explanation_depth": "medium",
"tone": "direct"
},
"technical": {
"languages": ["Go"],
"patterns": ["TDD", "microservices"],
"tools": ["docker", "git", "go test"]
},
"current_focus": {
"project": "odek",
"task": "extended memory module",
"blocker": null
},
"interaction_patterns": {
"common_openers": ["let's", "can you", "what if"],
"followup_after_refactor": "asks for tests",
"followup_after_bugfix": "asks for benchmark"
},
"pending_review": [
{
"id": "<pending-id>",
"field": "style.tone",
"value": "direct",
"evidence": "user said 'get to the point'",
"confidence": 0.9,
"created_at": "2026-01-01T12:00:00Z"
}
]
}When infer_user_state is enabled (default), the model is updated in a background goroutine every user_state_turn_interval turns or whenever the inferred project focus changes. The memory LLM receives the current model and recent trusted atoms and returns a JSON diff. Direct, scan-safe field updates are applied immediately; speculative or high-impact inferences are placed in pending_review with a field path, value, evidence, and confidence.
Pending reviews must be explicitly confirmed or rejected by the user. They are not recalled until confirmed.
odek memory extended pendinglists pending reviews.odek memory extended confirm <pending-id>applies the inference to the model and persists it.odek memory extended reject <pending-id>removes the inference without applying it.
The agent's memory tool can list pending reviews (list_pending_review) and reject them (reject_pending_review), but it cannot confirm them — confirmation is deliberately reserved for the human-gated CLI to prevent a prompt-injected agent from approving its own inferences.
Loaded and summarized user-model values are scanned for injection patterns; any field that fails the scan is dropped so a tampered user_model.json cannot poison the system prompt.
Atoms with a tainted source_class (tool_output, file_read, web, mcp, subagent, agent_generated, inferred) are stored in extended/quarantine.json instead of the live atom corpus.
A quarantined atom has the same schema as a trusted atom but:
source_classis one of the tainted classes.trust_boostis 0.- It is excluded from semantic search.
- It is subject to
quarantine_ttl_daysand may be auto-deleted.
Per-turn extraction only produces user_said atoms, so normal user messages do not land in quarantine. Quarantine is used when an atom is explicitly added (via the tool/API or programmatically) with a tainted source class, or when an inferred atom is produced by a future flow.
Promotion from quarantine to the live store is implemented and human-gated:
odek memory extended promote <atom-id>moves the atom to the live store withsource_class: user_approved.odek memory extended pin <atom-id>pins a live atom so it is never evicted.
Extended Memory enforces a hard disk budget. The default is 100 MB and is configurable via max_size_mb. The cap applies to the extended/ directory as a whole (atom chunks, vector store, metadata, quarantine, user model, and associations); existing facts/ and episodes/ keep their own lifecycle controls and are not counted.
| Component | Approx. Budget |
|---|---|
Atom chunks (chunks/*.md) |
~70 MB |
Vector store (vectors.gob + vectors.gob.emb) |
~20 MB |
Vector meta (vectors_meta.json) |
~1 MB |
Metadata (atoms.json) |
~5 MB |
Quarantine (quarantine.json) |
~1 MB |
User model (user_model.json) |
~1 MB |
Associations (associations.json) |
~1 MB |
At atom_max_chars: 300, this holds roughly 16,000-18,000 atoms.
The default policy is retention_decay. When a write would exceed max_size_mb, the module evicts atoms with the lowest retention score until the budget is met.
retention_score = confidence
* decay_factor
* trust_boost
pin_boost = infinity for pinned atoms, 1.0 otherwise
decay_factor = 0.5 ^ (age_days / decay_half_life_days)
trust_boost = 1.0 for user_said and user_approved
= 0.0 for inferred and all tainted source classes
Eviction order:
- Expired quarantined atoms (beyond
quarantine_ttl_days). - Lowest-scoring trusted atoms.
- Never evict: pinned atoms.
The vector index is rebuilt from surviving chunks in the background if a large eviction frees enough space (currently >10% of the corpus). The index skeleton (vectors_meta.json), quarantine file, user model, and associations file are always retained; their growth is bounded by the overall cap.
Extended Memory is configured under the memory.extended section.
Security note: Project-level
./odek.jsonis not allowed to set thememoryorembeddingsections (they could be used to redirect memory backends or poison the system prompt). Configurememory.extendedin~/.odek/config.json, viaODEK_MEMORY_EXTENDED_*environment variables, or with the CLI flags listed below.
{
"memory": {
"extended": {
"enabled": true,
"max_size_mb": 100,
"semantic_search_top_k": 10,
"semantic_search_overfetch": 4,
"semantic_search_min_score": 0.55,
"semantic_search_rerank": true,
"atom_max_chars": 300,
"memory_budget_chars": 2000,
"decay_half_life_days": 30,
"quarantine_ttl_days": 7,
"eviction_policy": "retention_decay",
"predictive_intents": 3,
"auto_extract_per_turn": true,
"infer_user_state": true,
"user_state_turn_interval": 5,
"user_state_max_pending": 20,
"associations_enabled": true,
"association_semantic_top_k": 3,
"proactive_return_after_break": true,
"style_mirroring_enabled": true,
"anaphora_resolution_enabled": true,
"follow_up_anticipation_enabled": true,
"llm": {
"base_url": "http://localhost:11434/v1",
"api_key": "",
"model": "qwen2.5:7b",
"max_tokens": 1024,
"temperature": 0.2,
"timeout_seconds": 30
},
"embedding": {
"provider": "http",
"base_url": "http://localhost:11434/v1",
"model": "nomic-embed-text"
}
}
}
}| Field | Default | Description |
|---|---|---|
enabled |
false |
Master switch for Extended Memory. |
max_size_mb |
100 |
Hard disk budget for the extended/ directory. |
semantic_search_top_k |
10 |
Number of atoms returned to the system prompt. |
semantic_search_overfetch |
4 |
Candidate multiplier before filtering and reranking. |
semantic_search_min_score |
0.55 |
Minimum cosine similarity for a candidate to be considered. |
semantic_search_rerank |
true |
Use the memory LLM to rerank candidates. |
atom_max_chars |
300 |
Maximum stored text length per atom. |
memory_budget_chars |
2000 |
Maximum injected memory context per turn. |
decay_half_life_days |
30 |
Days until an atom's recall/eviction weight halves, based on creation age. |
quarantine_ttl_days |
7 |
Days before a tainted atom is auto-deleted. |
eviction_policy |
"retention_decay" |
Eviction algorithm. "retention_decay" scores atoms by confidence, age-based decay, and trust; lowest scores are evicted first. Currently the only supported value. |
predictive_intents |
3 |
Maximum number of predicted follow-up intents generated per turn when follow_up_anticipation_enabled is true. |
auto_extract_per_turn |
true |
Extract atoms after every user message. |
infer_user_state |
true |
Enable background inference of the persistent user-state model. |
user_state_turn_interval |
5 |
Run user-state inference every N turns (and immediately on focus change). |
user_state_max_pending |
20 |
Maximum pending-review entries kept in the user model. |
associations_enabled |
true |
Enable bidirectional atom associations (temporal, task, semantic). |
association_semantic_top_k |
3 |
Number of semantic neighbours linked when building associations. |
proactive_return_after_break |
true |
On session resume, inject a "where you left off" summary. |
style_mirroring_enabled |
true |
Inject a style-guidance directive based on the inferred user model. |
anaphora_resolution_enabled |
true |
Resolve the first pronoun in a user message against recent trusted atoms when the top atom's score is high enough. |
follow_up_anticipation_enabled |
true |
Generate predicted intents and recall atoms for them. |
llm |
omitted | Dedicated memory LLM config. If omitted, the default global model is used. A warning is emitted if that model has thinking enabled. |
embedding |
omitted | Dedicated embedding backend. If omitted, uses the shared embedding config. |
A subset of memory.extended fields can be set from the CLI or environment:
| Config field | CLI flag | Environment variable |
|---|---|---|
enabled |
--memory-extended-enabled |
ODEK_MEMORY_EXTENDED_ENABLED |
max_size_mb |
--memory-extended-max-size-mb |
ODEK_MEMORY_EXTENDED_MAX_SIZE_MB |
atom_max_chars |
--memory-extended-atom-max-chars |
ODEK_MEMORY_EXTENDED_ATOM_MAX_CHARS |
memory_budget_chars |
--memory-extended-memory-budget-chars |
ODEK_MEMORY_EXTENDED_MEMORY_BUDGET_CHARS |
The existing memory tool is extended with new actions:
{
"name": "memory",
"parameters": {
"action": "add_atom | search_atoms | forget_atom | pin_atom | list_quarantine | confirm_pending_review | reject_pending_review | list_pending_review",
"content": "string",
"atom_type": "fact | preference | intent | decision | goal | convention | file | error | question",
"confidence": 0.0-1.0,
"query": "string",
"atom_id": "string",
"pending_id": "string"
}
}| Action | Parameters | Effect |
|---|---|---|
add_atom |
content (required), atom_type (default: fact), confidence (default: 1.0) |
Manually add a user-approved atom. |
search_atoms |
query (required) |
Explicit semantic search over the trusted atom corpus. |
forget_atom |
atom_id (required) |
Remove an atom by ID from the live store or quarantine. |
pin_atom |
atom_id (required) |
Pin a live atom so it is never evicted. |
list_quarantine |
— | List tainted atoms awaiting promotion. |
confirm_pending_review |
pending_id (required) |
Blocked: pending reviews must be confirmed via the CLI. |
reject_pending_review |
pending_id (required) |
Reject a pending user-model inference. |
list_pending_review |
— | List pending user-model inferences. |
Additional CLI commands:
odek memory extended forget <atom-id>
odek memory extended quarantine
odek memory extended compact
odek memory extended promote <atom-id>
odek memory extended pin <atom-id>
odek memory extended pending
odek memory extended confirm <pending-id>
odek memory extended reject <pending-id>| Command | Effect |
|---|---|
forget |
Removes an atom from the live store or quarantine. |
quarantine |
Lists tainted atoms awaiting promotion. |
compact |
Triggers a background rebuild of the vector index to reclaim space. |
promote |
Moves a quarantined atom to the live store as user_approved. |
pin |
Pins a live atom so it is never evicted. |
pending |
Lists pending user-model inferences. |
confirm |
Applies a pending user-model inference to the authoritative model. |
reject |
Removes a pending user-model inference without applying it. |
The odek memory promote <session-id> command that already exists promotes episodes, not atoms. Atom promotion and pinning are explicitly human-gated and not exposed as agent tool confirmations.
Once Extended Memory is enabled, the following proactive behaviors are active by default (each can be disabled via its config flag):
- Return after break: on session resume, a concise summary of where the user left off and the next likely step is injected as a system message (config:
proactive_return_after_break). - Anaphora resolution: the first pronoun in a user message (e.g. "that" or "it") is resolved against recent trusted atoms when the top atom's semantic similarity is above
semantic_search_min_score; otherwise the message is passed through unchanged. Only the first pronoun occurrence is replaced (config:anaphora_resolution_enabled). - Follow-up anticipation: the agent pre-loads related conventions, file references, and error patterns by generating predicted follow-up intents and recalling atoms for them (config:
follow_up_anticipation_enabled). - Style mirroring: tone, verbosity, formality, humor, and explanation depth inferred from the user model are injected as a style-guidance directive into the system prompt (config:
style_mirroring_enabled).
These behaviors are always data-driven by trusted atoms and the user model, never by tainted content.
Extended Memory inherits and extends the provenance model from MEMORY.md.
- Source-class tagging: every atom records where its content came from.
- Taint quarantine: indirect content is never embedded into the recallable vector space until promoted.
- Scan on write: every atom is scanned for injection patterns, invisible Unicode, and credential patterns before persistence. The optional prompt-injection guard (see docs/CONFIG.md) can apply a second opinion to the
memoryscope; when enabled, it covers atom extraction,add_atom, user-model inference, and anaphora resolution. - Untrusted wrapper: content from file reads, web fetches, MCP, and subagents is wrapped as untrusted before the main model ever sees it; the memory subsystem treats it as tainted.
- No self-promotion: the agent cannot promote a quarantined atom or confirm a pending user-model inference. Promotion and confirmation require an explicit user action or user message.
- Bounded storage: the size cap prevents a memory-DoS where an attacker fills disk with recalled junk.
- Dedicated LLM isolation: a compromised memory LLM can only add atoms; it cannot bypass provenance filtering or inject instructions into the main model's prompt.
For the best cost/latency trade-off:
{
"model": "claude-sonnet-4",
"base_url": "https://api.anthropic.com/v1",
"memory": {
"extended": {
"enabled": true,
"llm": {
"base_url": "http://localhost:11434/v1",
"model": "qwen2.5:7b",
"max_tokens": 1024,
"temperature": 0.2,
"timeout_seconds": 30
},
"embedding": {
"provider": "http",
"base_url": "http://localhost:11434/v1",
"model": "nomic-embed-text"
}
}
}
}This runs memory extraction, user-state inference, predictive intent generation, and embedding locally while the main agent uses a powerful remote reasoning model.
| Phase | Scope | Status |
|---|---|---|
| P0 — Atom store and dedicated LLM | Config schema, dedicated llm.Client wiring, atom schema, per-turn extraction, trusted write path, memory tool extensions. |
Implemented |
| P1 — Vector index and semantic recall | go-vector store over atoms, top-K semantic search, provenance filtering, min-score gate, optional LLM rerank. | Implemented |
| P2 — Size enforcement | 100 MB cap tracking, retention_decay eviction, background compaction. |
Implemented |
| P3 — User-state model | Background inference of a persistent user model, pending-review queue, user correction flow. | Implemented |
| P4 — Quarantine and promotion | Tainted atom quarantine, inline promotion commands, quarantine_ttl_days, atom pinning. |
Implemented |
| P5 — Predictive and proactive surfaces | Predicted-intent recall, return-after-break summary, anaphora resolution, style mirroring, follow-up anticipation. | Implemented |
Extended Memory does not replace the existing three-tier system; it augments it.
facts/user.mdandfacts/env.mdremain the frozen snapshot at session start.episodes/remains for session-level summaries.extended/adds fine-grained, searchable, anticipatory memory.
The per-turn system prompt injection order is:
- Frozen facts.
- Buffer summary.
- Style guidance (from the inferred user model, when
style_mirroring_enabledis true). - User-state block (from the inferred user model, when
infer_user_stateis true). - Episode summaries (if still enabled).
- Extended Memory atoms (ranked, budgeted).
Operators can disable Extended Memory at any time and fall back to the original behavior. Individual P3–P5 surfaces can also be disabled independently via their config flags.
- Should the 100 MB cap include or exclude the existing
episodes/andfacts/directories? Currently it only countsextended/. - Should inferred preferences require explicit confirmation, or should they be recallable immediately with a confidence threshold?
- Should quarantined atoms still be searchable via an explicit
memory search_quarantinetool? The currentquarantineCLI lists them but does not search by embedding. - Should associations be auto-discovered by cosine similarity only, or also extracted explicitly by the memory LLM?
- Should the user model support richer structured types (e.g., nested project history, timeline of blockers)?
These questions can be resolved behind config flags so operators can choose their preferred privacy/convenience trade-off.