Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
057825f
docs: add Extended Memory module reference proposal
jkyberneees Jul 5, 2026
260b9c7
docs: add Extended Memory module reference proposal
jkyberneees Jul 5, 2026
1ca8b5d
docs: add Extended Memory implementation plan
jkyberneees Jul 5, 2026
82f7f16
docs: update Extended Memory design with resolved architecture decisions
jkyberneees Jul 5, 2026
2029187
feat(extended-memory): align atom schema, source taxonomy, and on-dis…
jkyberneees Jul 9, 2026
c72fbd1
feat(extended-memory): wire memory LLM resolution and MemoryManager i…
jkyberneees Jul 9, 2026
917114c
feat(extended-memory): config, loop, tool, and CLI wiring
jkyberneees Jul 9, 2026
080dbc9
feat(extended-memory): P0-P2 implementation with security hardening a…
jkyberneees Jul 10, 2026
3a43cb0
extended: fail-closed cap enforcement when no evictable atoms remain
jkyberneees Jul 10, 2026
24494ec
odek memory: force extended memory enabled for CLI maintenance commands
jkyberneees Jul 10, 2026
e9f319e
extended: stabilize cap tests and add coverage tests
jkyberneees Jul 10, 2026
c7eb653
fix(extended-memory): transactional eviction and gofmt cleanup
jkyberneees Jul 10, 2026
fcd873b
chore: add .kai/ to .gitignore
jkyberneees Jul 10, 2026
0ca3095
fix(extended-memory): remove unused projectSize helper
jkyberneees Jul 10, 2026
07f706a
feat(extended-memory): implement P3-P5 anticipatory layer
jkyberneees Jul 10, 2026
a001ff9
fix(extended-memory): anaphora resolution, close race, recent-message…
jkyberneees Jul 10, 2026
bf2a46c
fix(extended-memory): scan user model state on load and fix quarantin…
jkyberneees Jul 10, 2026
6ca092a
fix(extended-memory): align SourceInferred trust boundary and wrap Re…
jkyberneees Jul 10, 2026
49e40fa
fix(extended-memory): use default topK/overfetch in anaphora score lo…
jkyberneees Jul 10, 2026
93ba487
docs(extended-memory): update P3-P5 implementation status and fix dea…
jkyberneees Jul 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,7 @@ docker/.odek/*
# Claude Code local artifacts
.claude/

sec_findings.md
sec_findings.md

# opencode agent local memory
.kai/
287 changes: 249 additions & 38 deletions cmd/odek/main.go

Large diffs are not rendered by default.

127 changes: 124 additions & 3 deletions cmd/odek/memory_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ package main
import (
"fmt"
"os"
"path/filepath"
"strings"

"github.com/BackendStack21/odek/internal/memory"
"github.com/BackendStack21/odek/internal/memory/extended"
)

// memoryCmd handles `odek memory <list|promote> [args]`.
// memoryCmd handles `odek memory <list|promote|extended> [args]`.
//
// This is the human-gated surface for the episode-memory trust control.
// Episodes whose originating session touched external content (web/http/MCP/
Expand All @@ -18,7 +20,7 @@ import (
// approve its own poisoned memory.
func memoryCmd(args []string) error {
if len(args) == 0 {
fmt.Fprintf(os.Stderr, "Usage: odek memory <list|promote> [args]\n")
fmt.Fprintf(os.Stderr, "Usage: odek memory <list|promote|extended> [args]\n")
return nil
}

Expand Down Expand Up @@ -60,7 +62,126 @@ func memoryCmd(args []string) error {
fmt.Printf("odek: promoted episode %q — it can now be recalled into future sessions\n", id)
return nil

case "extended":
return extendedMemoryCmd(dir, subArgs)

default:
return fmt.Errorf("unknown memory subcommand %q (expected: list, promote, extended)", sub)
}
}

// extendedMemoryCmd handles `odek memory extended forget|quarantine|compact`.
func extendedMemoryCmd(dir string, args []string) error {
if len(args) == 0 {
fmt.Fprintf(os.Stderr, "Usage: odek memory extended <forget|promote|pin|quarantine|compact|pending|confirm|reject> [args]\n")
return nil
}

sub := args[0]
subArgs := args[1:]

extDir := filepath.Join(dir, "extended")
cfg := extended.DefaultConfig()
enabled := true
cfg.Enabled = &enabled
em := extended.New(extDir, nil, cfg)

switch sub {
case "forget":
if len(subArgs) == 0 {
return fmt.Errorf("usage: odek memory extended forget <atom_id>")
}
id := subArgs[0]
if err := em.ForgetAtom(id); err != nil {
return err
}
fmt.Printf("odek: forgot atom %q\n", id)
return nil

case "promote":
if len(subArgs) == 0 {
return fmt.Errorf("usage: odek memory extended promote <atom_id>")
}
id := subArgs[0]
if err := em.PromoteAtom(id); err != nil {
return err
}
fmt.Printf("odek: promoted atom %q — it can now be recalled into future sessions\n", id)
return nil

case "pin":
if len(subArgs) == 0 {
return fmt.Errorf("usage: odek memory extended pin <atom_id>")
}
id := subArgs[0]
if err := em.PinAtom(id); err != nil {
return err
}
fmt.Printf("odek: pinned atom %q\n", id)
return nil

case "quarantine":
atoms, err := em.ListQuarantine()
if err != nil {
return err
}
if len(atoms) == 0 {
fmt.Println("No atoms in quarantine.")
return nil
}
fmt.Printf("%d atom(s) in quarantine (excluded from recall):\n\n", len(atoms))
for _, a := range atoms {
fmt.Printf("• %s [%s] %s\n", a.ID, a.SourceClass, truncate(a.Text, 120))
}
return nil

case "compact":
em.Compact()
fmt.Println("odek: Extended Memory vector index compaction triggered in the background")
return nil

case "pending":
pending, err := em.ListPendingReview()
if err != nil {
return err
}
if len(pending) == 0 {
fmt.Println("No pending user-model reviews.")
return nil
}
fmt.Printf("%d pending review(s):\n\n", len(pending))
for _, p := range pending {
fmt.Printf("• %s | %s = %q (confidence %.2f)\n", p.ID, p.Field, truncate(p.Value, 120), p.Confidence)
if p.Evidence != "" {
fmt.Printf(" evidence: %s\n", truncate(p.Evidence, 120))
}
}
fmt.Println("\nConfirm with: odek memory extended confirm <id>")
return nil

case "confirm":
if len(subArgs) == 0 {
return fmt.Errorf("usage: odek memory extended confirm <pending_id>")
}
id := subArgs[0]
if err := em.ConfirmPendingReview(id); err != nil {
return err
}
fmt.Printf("odek: confirmed pending review %q\n", id)
return nil

case "reject":
if len(subArgs) == 0 {
return fmt.Errorf("usage: odek memory extended reject <pending_id>")
}
id := subArgs[0]
if err := em.RejectPendingReview(id); err != nil {
return err
}
fmt.Printf("odek: rejected pending review %q\n", id)
return nil

default:
return fmt.Errorf("unknown memory subcommand %q (expected: list, promote)", sub)
return fmt.Errorf("unknown extended memory subcommand %q (expected: forget, promote, pin, quarantine, compact, pending, confirm, reject)", sub)
}
}
5 changes: 5 additions & 0 deletions cmd/odek/repl.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ func replCmd(args []string) error {
Skills: skillsCfg,
SkillManager: sm,
MemoryConfig: resolved.Memory,
MemoryDir: expandHome("~/.odek/memory"),
PromptCaching: resolved.PromptCaching,
})
if err != nil {
Expand Down Expand Up @@ -177,6 +178,10 @@ func replCmd(args []string) error {
sess.Sandbox = resolved.Sandbox
store.Save(sess)
}
cwd, _ := os.Getwd()
if mm := agent.Memory(); mm != nil {
mm.SetSessionContext(sess.ID, cwd)
}

fmt.Fprintf(os.Stderr, "\nodek ⚡ %s · session %s\n\n", modelLabel, sess.ID)
fmt.Fprintf(os.Stderr, " Type /help for commands, /exit to quit.\n\n")
Expand Down
6 changes: 6 additions & 0 deletions cmd/odek/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,7 @@ func newServeAgent(resolved config.ResolvedConfig, system string, sendFn func(v
Skills: &resolved.Skills,
SkillManager: sm,
MemoryConfig: resolved.Memory,
MemoryDir: expandHome("~/.odek/memory"),
ToolEventHandler: func(event, name, data string) {
sendFn(map[string]any{
"type": event,
Expand Down Expand Up @@ -967,6 +968,11 @@ func handlePrompt(
}
}

cwd, _ := os.Getwd()
if agent.Memory() != nil && sess != nil {
agent.Memory().SetSessionContext(sess.ID, cwd)
}

// Send session info
sid := ""
authToken := ""
Expand Down
71 changes: 71 additions & 0 deletions docs/CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ Every config knob has a `ODEK_*` counterpart:
| `ODEK_SANDBOX_CPUS` | `--sandbox-cpus` | string |
| `ODEK_SANDBOX_USER` | `--sandbox-user` | string |
| `ODEK_MAX_TOOL_PARALLEL` | `max_tool_parallel` | int |
| `ODEK_MEMORY_EXTENDED_ENABLED` | `--memory-extended-enabled` | bool |
| `ODEK_MEMORY_EXTENDED_MAX_SIZE_MB` | `--memory-extended-max-size-mb` | int |
| `ODEK_MEMORY_EXTENDED_ATOM_MAX_CHARS` | `--memory-extended-atom-max-chars` | int |
| `ODEK_MEMORY_EXTENDED_MEMORY_BUDGET_CHARS` | `--memory-extended-memory-budget-chars` | int |

## API key fallback order

Expand Down Expand Up @@ -252,6 +256,67 @@ The `memory` section controls the persistent memory system (see [docs/MEMORY.md]
| `episode_ttl_days` | 0 | Evict episodes older than this many days. `0` (default) disables TTL-based eviction. |
| `embedding` | *(inherits top-level `embedding`)* | Optional override of the embedding backend for episode recall, dedup, the non-LLM episode ranker, and fact merge-on-write. When unset, memory inherits the shared top-level [`embedding`](#shared-embedding-backend-embedding--memory-sessions--skills) default; if neither is set, local RandomProjections (lexical bag-of-words — fast, zero-cost, but no real semantics). See below. |

### Extended Memory (`memory.extended`)

`memory.extended` is an **opt-in** atomic memory layer. It extracts small, typed memory atoms from user messages and recalls them via semantic search over the atom corpus. It does not replace facts, the buffer, or episodes; it adds a fourth source of context that is injected after episodes on each turn. See [docs/EXTENDED_MEMORY.md](EXTENDED_MEMORY.md) for the full design.

> **Security note:** Project-level `./odek.json` cannot set the `memory` or `embedding` sections. Configure `memory.extended` in `~/.odek/config.json`, via the `ODEK_MEMORY_EXTENDED_*` environment variables, or with the CLI flags listed below.

```json
{
"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,
"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 | Env var | CLI flag | Description |
|-------|---------|---------|----------|-------------|
| `enabled` | `false` | `ODEK_MEMORY_EXTENDED_ENABLED` | `--memory-extended-enabled` | Master switch for Extended Memory. |
| `max_size_mb` | `100` | `ODEK_MEMORY_EXTENDED_MAX_SIZE_MB` | `--memory-extended-max-size-mb` | 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` | `ODEK_MEMORY_EXTENDED_ATOM_MAX_CHARS` | `--memory-extended-atom-max-chars` | Maximum stored text length per atom. |
| `memory_budget_chars` | `2000` | `ODEK_MEMORY_EXTENDED_MEMORY_BUDGET_CHARS` | `--memory-extended-memory-budget-chars` | Maximum injected Extended Memory context per turn. |
| `decay_half_life_days` | `30` | — | — | Days until an atom's recall/eviction weight halves. |
| `quarantine_ttl_days` | `7` | — | — | Days before a tainted atom is auto-deleted from quarantine. |
| `eviction_policy` | `"retention_decay"` | — | — | Eviction algorithm. `"retention_decay"` is the only supported value. |
| `predictive_intents` | `3` | — | — | Reserved for future predictive-intent recall (P5). Currently accepted but ignored. |
| `auto_extract_per_turn` | `true` | — | — | Extract atoms after every user message. |
| `infer_user_state` | `true` | — | — | Reserved for future user-state model inference (P3). Currently accepted but ignored. |
| `llm` | omitted | — | — | Dedicated memory LLM. If omitted, the main agent LLM is reused. A warning is emitted if that model has thinking enabled. |
| `embedding` | omitted | — | — | Dedicated embedding backend for atoms. If omitted, inherits `memory.embedding` or the shared top-level `embedding`. |

### `embedding` — real semantic embeddings (optional)

By default every similarity computation in memory uses go-vector
Expand Down Expand Up @@ -681,6 +746,12 @@ ODEK_SANDBOX=true odek run "run untrusted script"
# Enable skill learning via env var
ODEK_SKILLS_LEARN=true odek run "set up CI"

# Enable Extended Memory via CLI flag
odek run --memory-extended-enabled "remember that I prefer Go over Python"

# Or configure it globally in ~/.odek/config.json (memory cannot be set in ./odek.json)
# { "memory": { "extended": { "enabled": true } } }

# Sub-agent config (project-level)
echo '{"subagent": {"max_concurrency": 5, "timeout_seconds": 300}}' > ./odek.json

Expand Down
Loading
Loading