Yash030 commited on
Commit
12a6c9a
·
1 Parent(s): dc238fd

feat: migrate agentmemory to agentcache namespace, endpoints, and tools

Browse files
.clineskills CHANGED
@@ -1,20 +1,19 @@
1
- # Agent Memory Rules
2
 
3
- This workspace is integrated with long-term semantic memory via `agentmemory-python`.
4
- You must act as your own memory manager by calling the memory MCP tools at critical boundaries.
5
 
6
  ## Rules & Workflow
7
 
8
  1. **Initial Search (Prefetch Context)**:
9
- At the start of every session or new task, immediately call `memory_smart_search` with terms related to the current objective. This retrieves past architecture patterns, preferences, bug fixes, or lessons.
10
- - Example: `memory_smart_search(query="jwt token rotation logic")`
11
 
12
  2. **Lessons & Insights Capture**:
13
  When you successfully debug a complex error, discover an undocumented requirement, or establish a convention, persist it:
14
- - Call `memory_lesson_save` to record lessons that improve your coding capabilities. Duplicate saves strengthen confidence scores.
15
- - Call `memory_save` to save long-term structural facts. Always extract 2-5 specific lowercased tags (e.g. `auth-flow`, `refresh-token`) as concepts.
16
 
17
  3. **Checklist Before Ending**:
18
  Before stating a task is complete:
19
- - Reflect on whether any lessons learned should be saved.
20
- - Call `memory_reflect` to automatically distribute observations into slots if needed.
 
1
+ # Agent Cache Rules
2
 
3
+ This workspace is integrated with long-term semantic memory via `agentcache-python`.
4
+ You must act as your own memory manager by calling the cache MCP tools at critical boundaries.
5
 
6
  ## Rules & Workflow
7
 
8
  1. **Initial Search (Prefetch Context)**:
9
+ At the start of every session or new task, immediately call `cache_smart_search` with terms related to the current objective. This retrieves past architecture patterns, preferences, bug fixes, or lessons.
10
+ - Example: `cache_smart_search(query="jwt token rotation logic")`
11
 
12
  2. **Lessons & Insights Capture**:
13
  When you successfully debug a complex error, discover an undocumented requirement, or establish a convention, persist it:
14
+ - Call `cache_save` to save long-term structural facts. Always extract 2-5 specific lowercased tags (e.g. `auth-flow`, `refresh-token`) as concepts.
 
15
 
16
  3. **Checklist Before Ending**:
17
  Before stating a task is complete:
18
+ - Reflect on whether any lessons learned or insights should be saved.
19
+ - Call `cache_dedup` to deduplicate any observations if needed.
.cursorrules CHANGED
@@ -1,20 +1,23 @@
1
- # Agent Memory Rules
2
 
3
- This workspace is integrated with long-term semantic memory via `agentmemory-python`.
4
- You must act as your own memory manager by calling the memory MCP tools at critical boundaries.
5
 
6
  ## Rules & Workflow
7
 
8
  1. **Initial Search (Prefetch Context)**:
9
- At the start of every session or new task, immediately call `memory_smart_search` with terms related to the current objective. This retrieves past architecture patterns, preferences, bug fixes, or lessons.
10
- - Example: `memory_smart_search(query="jwt token rotation logic")`
11
 
12
- 2. **Lessons & Insights Capture**:
 
 
 
 
13
  When you successfully debug a complex error, discover an undocumented requirement, or establish a convention, persist it:
14
- - Call `memory_lesson_save` to record lessons that improve your coding capabilities. Duplicate saves strengthen confidence scores.
15
- - Call `memory_save` to save long-term structural facts. Always extract 2-5 specific lowercased tags (e.g. `auth-flow`, `refresh-token`) as concepts.
16
 
17
- 3. **Checklist Before Ending**:
18
  Before stating a task is complete:
19
- - Reflect on whether any lessons learned should be saved.
20
- - Call `memory_reflect` to automatically distribute observations into slots if needed.
 
1
+ # Agent Cache Rules
2
 
3
+ This workspace is integrated with long-term semantic memory via `agentcache-python`.
4
+ You must act as your own memory manager by calling the cache MCP tools at critical boundaries.
5
 
6
  ## Rules & Workflow
7
 
8
  1. **Initial Search (Prefetch Context)**:
9
+ At the start of every session or new task, immediately call `cache_smart_search` with terms related to the current objective. This retrieves past architecture patterns, preferences, bug fixes, and past work.
10
+ - Example: `cache_smart_search(query="jwt token rotation logic")`
11
 
12
+ 2. **Log Observations**:
13
+ As you work, call `agent_observe` to record what you are doing. Include `folderPath` (the working directory), `agentId` (your agent name), and `text` (what happened).
14
+ - Example: `agent_observe(folderPath="/your/project/path", agentId="cursor", text="Refactored auth middleware to use JWT rotation")`
15
+
16
+ 3. **Save Long-Term Memories**:
17
  When you successfully debug a complex error, discover an undocumented requirement, or establish a convention, persist it:
18
+ - Call `cache_save` to save long-term structural facts. Always extract 2-5 specific lowercased tags (e.g. `auth-flow`, `refresh-token`) as concepts.
 
19
 
20
+ 4. **Checklist Before Ending**:
21
  Before stating a task is complete:
22
+ - Reflect on whether any insights should be saved via `cache_save`.
23
+ - Review the folder activity feed with `cache_timeline` to confirm all significant work was captured.
.env.example CHANGED
@@ -1,16 +1,16 @@
1
  # =============================================================================
2
- # agentmemory configuration
3
  # =============================================================================
4
  #
5
- # Copy this file to `~/.agentmemory/.env` (or to your project root if you
6
  # prefer scoped config) and uncomment the lines you want to override.
7
  #
8
- # Every line is OFF by default — `agentmemory` runs out of the box with no
9
  # LLM key, no embedding key, and no API auth. Set keys here only when you
10
  # want to enable the corresponding feature.
11
  #
12
- # Run `npx @agentmemory/agentmemory init` to copy this file into place
13
- # automatically. Run `npx @agentmemory/agentmemory doctor` to verify that
14
  # the daemon reads the env you expect.
15
  #
16
  # Defaults shown in comments. Listed in priority order — the first key
@@ -20,7 +20,7 @@
20
  # 1. LLM provider — pick ONE
21
  # -----------------------------------------------------------------------------
22
  #
23
- # Without a provider key, agentmemory runs in noop mode: observations are
24
  # indexed via zero-LLM synthetic compression, hybrid search still works,
25
  # but LLM-backed summarisation / reflection / consolidation are disabled.
26
  # The detection order is OPENAI_API_KEY → MINIMAX_API_KEY → ANTHROPIC_API_KEY
@@ -49,12 +49,12 @@
49
  # (Gemini, OpenRouter, MiniMax, OpenAI LLM, and OpenAI/Cohere/Voyage/OpenRouter
50
  # embedding). The OpenAI LLM path also honours the OpenAI-scoped
51
  # OPENAI_TIMEOUT_MS alias for back-compat with v0.9.17 (precedence).
52
- # AGENTMEMORY_LLM_TIMEOUT_MS=60000 # Default: 60 000 ms (60 s)
53
 
54
  # Opt-in Claude-subscription fallback (spawns @anthropic-ai/claude-agent-sdk
55
  # child sessions). Off by default — the agent-sdk fallback can trigger
56
  # Stop-hook recursion (#149 follow-up) when invoked from inside Claude Code.
57
- # AGENTMEMORY_ALLOW_AGENT_SDK=true
58
 
59
  # FALLBACK_PROVIDERS=anthropic,gemini # Comma-separated chain tried after the primary provider returns an error (e.g. rate limit)
60
 
@@ -62,7 +62,7 @@
62
  # 2. Embedding provider — auto-detected, override via EMBEDDING_PROVIDER
63
  # -----------------------------------------------------------------------------
64
  #
65
- # Without an embedding key, agentmemory runs in BM25-only mode for hybrid
66
  # search. Detection order: EMBEDDING_PROVIDER override → GEMINI_API_KEY →
67
  # OPENAI_API_KEY → VOYAGE_API_KEY → COHERE_API_KEY → OPENROUTER_API_KEY →
68
  # local (Xenova/all-MiniLM-L6-v2, 384-dim).
@@ -87,7 +87,7 @@
87
  # Without a secret, REST endpoints are open on loopback. Set this when
88
  # you expose the daemon beyond loopback or run behind a reverse proxy.
89
 
90
- # AGENTMEMORY_SECRET=your-secret-here
91
 
92
  # -----------------------------------------------------------------------------
93
  # 4. Search tuning
@@ -95,7 +95,7 @@
95
 
96
  # BM25_WEIGHT=0.4 # Hybrid search weight for BM25 leg
97
  # VECTOR_WEIGHT=0.6 # Hybrid search weight for vector leg
98
- # AGENTMEMORY_GRAPH_WEIGHT=0.2 # Graph traversal bonus on smart-search ranking
99
  # TOKEN_BUDGET=2000 # Max tokens injected via mem::context per session
100
  # MAX_OBS_PER_SESSION=500 # Per-session observation cap before consolidation kicks in
101
  # SUMMARIZE_CHUNK_SIZE=400 # When mem::summarize sees a session larger than this, it chunks observations and map-reduces (chunk-summarize → reduce-merge) to stay within the LLM's context window. Default 400 ≈ 50k tokens per chunk at ~110 tok/obs. Native sessions are capped by MAX_OBS_PER_SESSION; chunking primarily matters for bulk-imported jsonl sessions, which bypass that cap.
@@ -105,35 +105,35 @@
105
  # 5. Behaviour flags
106
  # -----------------------------------------------------------------------------
107
 
108
- # AGENTMEMORY_AUTO_COMPRESS=true # Run LLM compression on every observation batch (requires a provider key). Default off — synthetic compression handles most cases.
109
- # AGENTMEMORY_INJECT_CONTEXT=true # Inject recalled memories back into agent prompts (#143). Default off — hooks capture observations but do not modify conversation.
110
  # CONSOLIDATION_ENABLED=true # Run the 4-tier consolidation pipeline (memories → semantic → procedural). Default off — opt in once you've measured the LLM cost.
111
  # CONSOLIDATION_DECAY_DAYS=30 # Age (days) after which non-reinforced memories decay during consolidation
112
  # GRAPH_EXTRACTION_ENABLED=true # Extract concept-graph edges on remember; powers the graph-traversal recall path
113
  # GRAPH_EXTRACTION_BATCH_SIZE=8 # Memories per graph-extraction batch
114
- # AGENTMEMORY_REFLECT=true # Periodically auto-synthesize lessons from memories
115
- # AGENTMEMORY_DROP_STALE_INDEX=true # Drop on-disk BM25 / vector index on startup if dim guard fires (#248). Recovery toggle for stuck-state debugging.
116
- # AGENTMEMORY_IMAGE_EMBEDDINGS=true # Enable image embeddings when an image provider is present (experimental).
117
 
118
  # -----------------------------------------------------------------------------
119
  # 6. CLI / runtime knobs
120
  # -----------------------------------------------------------------------------
121
 
122
- # AGENTMEMORY_TOOLS=all # core (7 tools, default) | all (51 tools) — surface exposed to MCP clients
123
- # AGENTMEMORY_SLOTS=memory # Comma-separated plugin slot names the CLI should claim
124
- # AGENTMEMORY_DEBUG=1 # Trace MCP shim probe + standalone fallback decisions to stderr
125
- # AGENTMEMORY_FORCE_PROXY=1 # Skip the MCP shim livez probe and trust AGENTMEMORY_URL (for sandboxed MCP clients that can't reach localhost)
126
- # AGENTMEMORY_PROBE_TIMEOUT_MS=2000 # MCP shim livez probe timeout
127
- # AGENTMEMORY_URL=http://localhost:3111 # REST base URL — honored by status, doctor, MCP shim
128
- # AGENTMEMORY_VIEWER_URL=http://localhost:3113 # Override the viewer URL printed by `agentmemory status`
129
- # AGENTMEMORY_EXPORT_ROOT=~/agentmemory-backup # Default destination for `agentmemory export`
130
 
131
- # STANDALONE_MCP=1 # MCP shim only — bypass the worker and run @agentmemory/mcp in-process
132
- # STANDALONE_PERSIST_PATH=~/.agentmemory/local.db # Path used by the standalone MCP shim's local fallback store
133
 
134
  # Snapshot exporter — periodic snapshots of state_store + stream_store.
135
  # SNAPSHOT_ENABLED=true
136
- # SNAPSHOT_DIR=~/.agentmemory/snapshots
137
  # SNAPSHOT_INTERVAL=3600 # Seconds between snapshots
138
 
139
  # Team sharing — when set, memories are scoped to (TEAM_ID, USER_ID) tuples.
@@ -153,12 +153,12 @@
153
  # 8. iii engine pin
154
  # -----------------------------------------------------------------------------
155
  #
156
- # agentmemory currently pins iii-engine to v0.11.2 — v0.11.6 introduces a
157
- # new sandbox-everything-via-`iii worker add` model that agentmemory
158
- # hasn't been refactored for yet. Override with AGENTMEMORY_III_VERSION
159
  # only after migrating to the sandbox model manually.
160
 
161
- # AGENTMEMORY_III_VERSION=0.11.2
162
 
163
  # -----------------------------------------------------------------------------
164
  # 9. Claude Code bridge (opt-in)
 
1
  # =============================================================================
2
+ # agentcache configuration
3
  # =============================================================================
4
  #
5
+ # Copy this file to `~/.agentcache/.env` (or to your project root if you
6
  # prefer scoped config) and uncomment the lines you want to override.
7
  #
8
+ # Every line is OFF by default — `agentcache` runs out of the box with no
9
  # LLM key, no embedding key, and no API auth. Set keys here only when you
10
  # want to enable the corresponding feature.
11
  #
12
+ # Run `npx @agentcache/agentcache init` to copy this file into place
13
+ # automatically. Run `npx @agentcache/agentcache doctor` to verify that
14
  # the daemon reads the env you expect.
15
  #
16
  # Defaults shown in comments. Listed in priority order — the first key
 
20
  # 1. LLM provider — pick ONE
21
  # -----------------------------------------------------------------------------
22
  #
23
+ # Without a provider key, agentcache runs in noop mode: observations are
24
  # indexed via zero-LLM synthetic compression, hybrid search still works,
25
  # but LLM-backed summarisation / reflection / consolidation are disabled.
26
  # The detection order is OPENAI_API_KEY → MINIMAX_API_KEY → ANTHROPIC_API_KEY
 
49
  # (Gemini, OpenRouter, MiniMax, OpenAI LLM, and OpenAI/Cohere/Voyage/OpenRouter
50
  # embedding). The OpenAI LLM path also honours the OpenAI-scoped
51
  # OPENAI_TIMEOUT_MS alias for back-compat with v0.9.17 (precedence).
52
+ # AGENTCACHE_LLM_TIMEOUT_MS=60000 # Default: 60 000 ms (60 s)
53
 
54
  # Opt-in Claude-subscription fallback (spawns @anthropic-ai/claude-agent-sdk
55
  # child sessions). Off by default — the agent-sdk fallback can trigger
56
  # Stop-hook recursion (#149 follow-up) when invoked from inside Claude Code.
57
+ # AGENTCACHE_ALLOW_AGENT_SDK=true
58
 
59
  # FALLBACK_PROVIDERS=anthropic,gemini # Comma-separated chain tried after the primary provider returns an error (e.g. rate limit)
60
 
 
62
  # 2. Embedding provider — auto-detected, override via EMBEDDING_PROVIDER
63
  # -----------------------------------------------------------------------------
64
  #
65
+ # Without an embedding key, agentcache runs in BM25-only mode for hybrid
66
  # search. Detection order: EMBEDDING_PROVIDER override → GEMINI_API_KEY →
67
  # OPENAI_API_KEY → VOYAGE_API_KEY → COHERE_API_KEY → OPENROUTER_API_KEY →
68
  # local (Xenova/all-MiniLM-L6-v2, 384-dim).
 
87
  # Without a secret, REST endpoints are open on loopback. Set this when
88
  # you expose the daemon beyond loopback or run behind a reverse proxy.
89
 
90
+ # AGENTCACHE_SECRET=your-secret-here
91
 
92
  # -----------------------------------------------------------------------------
93
  # 4. Search tuning
 
95
 
96
  # BM25_WEIGHT=0.4 # Hybrid search weight for BM25 leg
97
  # VECTOR_WEIGHT=0.6 # Hybrid search weight for vector leg
98
+ # AGENTCACHE_GRAPH_WEIGHT=0.2 # Graph traversal bonus on smart-search ranking
99
  # TOKEN_BUDGET=2000 # Max tokens injected via mem::context per session
100
  # MAX_OBS_PER_SESSION=500 # Per-session observation cap before consolidation kicks in
101
  # SUMMARIZE_CHUNK_SIZE=400 # When mem::summarize sees a session larger than this, it chunks observations and map-reduces (chunk-summarize → reduce-merge) to stay within the LLM's context window. Default 400 ≈ 50k tokens per chunk at ~110 tok/obs. Native sessions are capped by MAX_OBS_PER_SESSION; chunking primarily matters for bulk-imported jsonl sessions, which bypass that cap.
 
105
  # 5. Behaviour flags
106
  # -----------------------------------------------------------------------------
107
 
108
+ # AGENTCACHE_AUTO_COMPRESS=true # Run LLM compression on every observation batch (requires a provider key). Default off — synthetic compression handles most cases.
109
+ # AGENTCACHE_INJECT_CONTEXT=true # Inject recalled memories back into agent prompts (#143). Default off — hooks capture observations but do not modify conversation.
110
  # CONSOLIDATION_ENABLED=true # Run the 4-tier consolidation pipeline (memories → semantic → procedural). Default off — opt in once you've measured the LLM cost.
111
  # CONSOLIDATION_DECAY_DAYS=30 # Age (days) after which non-reinforced memories decay during consolidation
112
  # GRAPH_EXTRACTION_ENABLED=true # Extract concept-graph edges on remember; powers the graph-traversal recall path
113
  # GRAPH_EXTRACTION_BATCH_SIZE=8 # Memories per graph-extraction batch
114
+ # AGENTCACHE_REFLECT=true # Periodically auto-synthesize lessons from memories
115
+ # AGENTCACHE_DROP_STALE_INDEX=true # Drop on-disk BM25 / vector index on startup if dim guard fires (#248). Recovery toggle for stuck-state debugging.
116
+ # AGENTCACHE_IMAGE_EMBEDDINGS=true # Enable image embeddings when an image provider is present (experimental).
117
 
118
  # -----------------------------------------------------------------------------
119
  # 6. CLI / runtime knobs
120
  # -----------------------------------------------------------------------------
121
 
122
+ # AGENTCACHE_TOOLS=all # core (7 tools, default) | all (51 tools) — surface exposed to MCP clients
123
+ # AGENTCACHE_SLOTS=memory # Comma-separated plugin slot names the CLI should claim
124
+ # AGENTCACHE_DEBUG=1 # Trace MCP shim probe + standalone fallback decisions to stderr
125
+ # AGENTCACHE_FORCE_PROXY=1 # Skip the MCP shim livez probe and trust AGENTCACHE_URL (for sandboxed MCP clients that can't reach localhost)
126
+ # AGENTCACHE_PROBE_TIMEOUT_MS=2000 # MCP shim livez probe timeout
127
+ # AGENTCACHE_URL=http://localhost:3111 # REST base URL — honored by status, doctor, MCP shim
128
+ # AGENTCACHE_VIEWER_URL=http://localhost:3113 # Override the viewer URL printed by `agentcache status`
129
+ # AGENTCACHE_EXPORT_ROOT=~/agentcache-backup # Default destination for `agentcache export`
130
 
131
+ # STANDALONE_MCP=1 # MCP shim only — bypass the worker and run @agentcache/mcp in-process
132
+ # STANDALONE_PERSIST_PATH=~/.agentcache/local.db # Path used by the standalone MCP shim's local fallback store
133
 
134
  # Snapshot exporter — periodic snapshots of state_store + stream_store.
135
  # SNAPSHOT_ENABLED=true
136
+ # SNAPSHOT_DIR=~/.agentcache/snapshots
137
  # SNAPSHOT_INTERVAL=3600 # Seconds between snapshots
138
 
139
  # Team sharing — when set, memories are scoped to (TEAM_ID, USER_ID) tuples.
 
153
  # 8. iii engine pin
154
  # -----------------------------------------------------------------------------
155
  #
156
+ # agentcache currently pins iii-engine to v0.11.2 — v0.11.6 introduces a
157
+ # new sandbox-everything-via-`iii worker add` model that agentcache
158
+ # hasn't been refactored for yet. Override with AGENTCACHE_III_VERSION
159
  # only after migrating to the sandbox model manually.
160
 
161
+ # AGENTCACHE_III_VERSION=0.11.2
162
 
163
  # -----------------------------------------------------------------------------
164
  # 9. Claude Code bridge (opt-in)
.kiro/specs/folder-based-memory/design.md CHANGED
@@ -10,6 +10,45 @@ The redesign eliminates sessions, lessons, slots, actions, crystals, and artefac
10
 
11
  ## Architecture
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  ### High-Level System Diagram
14
 
15
  ```mermaid
@@ -20,7 +59,7 @@ graph TD
20
  A3[Cursor]
21
  end
22
 
23
- subgraph MCP / REST API ["MCP / REST API (src/app.py)"]
24
  T1[agent_observe]
25
  T2[memory_recall]
26
  T3[memory_save / agent_remember]
@@ -28,62 +67,70 @@ graph TD
28
  T5[memory_export]
29
  T6[memory_forget]
30
  T7[memory_diagnose]
 
 
 
31
  end
32
 
33
- subgraph Business Logic ["Business Logic (src/functions.py)"]
34
  F1[folder_observe]
35
  F2[folder_search]
36
  F3[remember / forget]
37
  F4[folder_timeline]
38
  F5[folder_graph_build]
39
- F6[health_check / export]
40
  F7[strip_private_data]
41
- F8[IndexPersistence]
42
  end
43
 
44
- subgraph Storage ["Storage (src/db.py — SQLite KV)"]
45
  S1["mem:folders (index)"]
46
- S2["mem:folder:{path}:{agent} (observations + metadata)"]
47
- S3[mem:memories]
48
- S4["mem:index:bm25:* (sharded BM25)"]
49
- S5[audit_log]
 
 
50
  end
51
 
52
- subgraph Search ["Search (src/search.py)"]
53
- SR1[SearchIndex BM25]
54
- SR2[VectorIndex cosine]
55
- SR3[HybridSearch RRF]
 
 
 
56
  end
57
 
58
- subgraph Viewer ["Viewer (src/viewer/index.html)"]
59
  V1[Folders tab]
60
  V2[Memories tab]
61
  V3[Graph tab]
62
  V4[Timeline tab]
63
  end
64
 
65
- A1 & A2 & A3 --> T1 & T2 & T3 & T4 & T5 & T6 & T7
66
  T1 --> F1
67
  T2 & T4 --> F2
68
  T3 --> F3
69
  T5 --> F6
70
  T6 --> F3
71
  T7 --> F6
 
 
72
 
73
  F1 --> F7
74
- F1 --> S2
75
  F1 --> SR1 & SR2
76
- F1 --> S1
77
- F3 --> S3
78
  F2 --> SR3
79
  SR1 & SR2 --> SR3
80
- F8 --> S4
 
81
  SR1 & SR2 --> F8
82
 
83
- F4 --> S2
84
- F5 --> S1 & S2 & S3
85
-
86
- S2 & S3 & S4 & S5 --> Storage
87
 
88
  Viewer --> T2 & T4
89
  F1 -.->|WebSocket broadcast| Viewer
@@ -150,26 +197,28 @@ sequenceDiagram
150
 
151
  ## Components and Interfaces
152
 
153
- ### Component 1: KV Scope Registry (src/functions.py — `KV` class)
154
 
155
- **Purpose**: Single source of truth for all KV scope names.
156
 
157
- **New scopes (replaces session-based scopes):**
158
 
159
  ```python
160
  class KV:
161
- # Folder memory index — stores list of all known folder_paths
162
  folders = "mem:folders"
163
 
164
- # Per-(folder, agent) observations list
165
- # key = obs_id, value = observation dict
 
 
166
  @staticmethod
167
  def folder_obs(folder_path: str, agent_id: str) -> str:
168
  safe_path = folder_path.replace("\\", "/").strip("/")
169
  safe_agent = agent_id.strip()
170
  return f"mem:folder:{safe_path}:{safe_agent}"
171
 
172
- # Per-(folder, agent) metadata (last_updated, summary, obs_count)
173
  @staticmethod
174
  def folder_meta(folder_path: str, agent_id: str) -> str:
175
  safe_path = folder_path.replace("\\", "/").strip("/")
@@ -179,16 +228,33 @@ class KV:
179
  # Global long-term memories (unchanged)
180
  memories = "mem:memories"
181
 
182
- # BM25 index shards (unchanged)
183
  bm25Index = "mem:index:bm25"
184
 
185
- # Audit scope (unchanged)
186
  audit = "mem:audit"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  ```
188
 
189
- **Scopes removed**: `sessions`, `obs:{session_id}`, `lessons`, `slots`, `slots:global`, `slots:{project}`, `actions`, `action-edges`, `crystals`, `sketches`, `facets`, `sentinels`, `signals`, `checkpoints`, `routines`, `routine-runs`, `profiles`, `summaries`, `semantic`, `procedural`, `commits`, `leases`, `mesh`, `recent-searches`, `claude-bridge`, `insights`, `retention`, `access`, `image-refs`
190
 
191
- **Scopes kept**: `memories`, `index:bm25:*`, `audit`, `relations` (repurposed for folder graph edges)
192
 
193
  ---
194
 
@@ -797,32 +863,62 @@ END
797
 
798
  ---
799
 
800
- ## MCP Tools: Kept, Renamed, and Dropped
 
 
801
 
802
- ### Kept / Repurposed (same tool name, redirected to folder storage)
803
 
804
- | Tool | New Behavior |
805
- |------|-------------|
806
- | `agent_observe` | Required: `folderPath`, `agentId`, `text`. Logs to `(folder_path, agent_id)` scope. `sessionId` accepted but ignored. |
807
- | `memory_recall` | Searches across all folder observations + global memories via BM25+vector. Optional `folderPath` / `agentId` filters. |
808
- | `memory_save` | Unchanged saves to global memories |
809
- | `agent_remember` | Unchanged saves to global memories |
810
- | `memory_smart_search` | Same as `memory_recall` (hybrid search) |
811
- | `memory_export` | Exports folders + observations + memories in v2 format |
812
- | `memory_forget` | Accepts `memoryId` (global) OR `folderPath+agentId` (folder observations) |
813
- | `memory_diagnose` | Returns folder/agent/observation counts instead of session counts |
 
 
 
814
 
815
- ### New Tools
816
 
817
- | Tool | Description |
818
- |------|-------------|
819
- | `memory_folders` | List all (folder, agent) pairs with obsCount and lastUpdated |
820
- | `memory_folder_observations` | Get observations for a specific `(folderPath, agentId)` pair |
821
- | `memory_timeline` | Folder activity feed — observations sorted by time, filterable by folderPath/agentId |
822
 
823
- ### Dropped Tools (removed from schema)
824
 
825
- `memory_sessions`, `memory_sessions_list`, `memory_observations` (old session-based version), `memory_profile`, `memory_lessons`, `memory_lesson_save`, `memory_lesson_recall`, `memory_lesson_search`, `memory_consolidate`, `memory_reflect`, `memory_crystallize`, `memory_slot_list`, `memory_slot_get`, `memory_slot_create`, `memory_slot_append`, `memory_slot_replace`, `memory_slot_delete`, `memory_action_create`, `memory_action_update`, `memory_frontier`, `memory_antigravity_sync`, `memory_antigravity_sync_all`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
826
 
827
  ---
828
 
 
10
 
11
  ## Architecture
12
 
13
+ ### Implemented Module Layout
14
+
15
+ ```
16
+ src/
17
+ ├── app.py Thin Flask factory — create_app(), WebSocket, CORS
18
+ ├── workers.py Background threads — index rebuild, auto-forget, SIGTERM handler
19
+ ├── cli.py CLI entrypoint (agentmemory serve/migrate/export)
20
+ ├── connect.py Client connection helper
21
+ ├── db.py StateKV — SQLite WAL, per-thread connections, stats()
22
+ ├── functions.py All canonical business logic (large; split planned)
23
+ ├── search.py BM25 + VectorIndex + HybridSearch + 3 embedding providers
24
+ ├── viewer_helpers.py Viewer HTML injection helper
25
+
26
+ ├── routes/ Flask blueprints (A1 complete)
27
+ │ ├── observations.py /observe, /agent/observe, /folders, /folder/observations
28
+ │ ├── memories.py /remember, /agent/remember, /memories, /forget
29
+ │ ├── search.py /search, /timeline
30
+ │ ├── graph.py /graph, /graph/stats, /graph/query, /graph/build
31
+ │ ├── health.py /livez, /health, /audit, /config/flags
32
+ │ ├── mcp.py /mcp/tools GET+POST (12 active tools)
33
+ │ └── migration.py /migrate
34
+
35
+ ├── memory/ Thin shim package re-exporting from functions.py (A2.1)
36
+ │ ├── observe.py folder_observe, observe, build_synthetic_compression, strip_private_data
37
+ │ ├── remember.py remember, forget, jaccard_similarity
38
+ │ ├── context.py context, export_data, rebuild_index
39
+ │ ├── graph.py folder_graph_build
40
+ │ ├── timeline.py folder_timeline, folder_search
41
+ │ └── health.py health_check, auto_forget
42
+
43
+ ├── storage/ KV scope registry + path utilities (A2.3)
44
+ │ ├── scopes.py KV class (copy; canonical in functions.py)
45
+ │ ├── paths.py normalize_folder_path, validate_agent_id, generate_id, fingerprint_id
46
+ │ └── images.py save_image_to_disk, delete_image, touch_image
47
+
48
+ └── viewer/
49
+ └── index.html Single-file HTML dashboard (4 tabs: Folders/Memories/Graph/Timeline)
50
+ ```
51
+
52
  ### High-Level System Diagram
53
 
54
  ```mermaid
 
59
  A3[Cursor]
60
  end
61
 
62
+ subgraph "MCP / REST API (src/routes/)"
63
  T1[agent_observe]
64
  T2[memory_recall]
65
  T3[memory_save / agent_remember]
 
67
  T5[memory_export]
68
  T6[memory_forget]
69
  T7[memory_diagnose]
70
+ T8[memory_folders]
71
+ T9[memory_folder_observations]
72
+ T10[memory_timeline]
73
  end
74
 
75
+ subgraph "Business Logic (src/functions.py)"
76
  F1[folder_observe]
77
  F2[folder_search]
78
  F3[remember / forget]
79
  F4[folder_timeline]
80
  F5[folder_graph_build]
81
+ F6[health_check / export_data]
82
  F7[strip_private_data]
83
+ F8[IndexPersistence — debounced 5s]
84
  end
85
 
86
+ subgraph "Storage (src/db.py — SQLite KV)"
87
  S1["mem:folders (index)"]
88
+ S2["mem:folder:{path}:{agent} (observations)"]
89
+ S3["mem:foldermeta:{path}:{agent} (metadata)"]
90
+ S4[mem:memories]
91
+ S5["mem:index:bm25:* (sharded BM25)"]
92
+ S6["mem:obs_lookup (O(1) hydration)"]
93
+ S7[audit_log]
94
  end
95
 
96
+ subgraph "Search (src/search.py)"
97
+ SR1[SearchIndex BM25+stemmer+synonyms]
98
+ SR2[VectorIndex cosine sim]
99
+ SR3[HybridSearch RRF k=60]
100
+ EP1[GeminiEmbeddingProvider 768d]
101
+ EP2[OpenAIEmbeddingProvider 1536d]
102
+ EP3[SentenceTransformerProvider variable]
103
  end
104
 
105
+ subgraph "Viewer (src/viewer/index.html)"
106
  V1[Folders tab]
107
  V2[Memories tab]
108
  V3[Graph tab]
109
  V4[Timeline tab]
110
  end
111
 
112
+ A1 & A2 & A3 --> T1 & T2 & T3 & T4 & T5 & T6 & T7 & T8 & T9 & T10
113
  T1 --> F1
114
  T2 & T4 --> F2
115
  T3 --> F3
116
  T5 --> F6
117
  T6 --> F3
118
  T7 --> F6
119
+ T8 & T9 --> S1 & S2
120
+ T10 --> F4
121
 
122
  F1 --> F7
123
+ F1 --> S2 & S3 & S1 & S6
124
  F1 --> SR1 & SR2
125
+ F3 --> S4
 
126
  F2 --> SR3
127
  SR1 & SR2 --> SR3
128
+ EP1 & EP2 & EP3 --> SR2
129
+ F8 --> S5
130
  SR1 & SR2 --> F8
131
 
132
+ F4 --> S1 & S2
133
+ F5 --> S1 & S2 & S4
 
 
134
 
135
  Viewer --> T2 & T4
136
  F1 -.->|WebSocket broadcast| Viewer
 
197
 
198
  ## Components and Interfaces
199
 
200
+ ### Component 1: KV Scope Registry (src/functions.py + src/storage/scopes.py — `KV` class)
201
 
202
+ **Purpose**: Single source of truth for all KV scope names. Defined in `src/functions.py` (canonical) and mirrored to `src/storage/scopes.py` (A2.3).
203
 
204
+ **Active scopes:**
205
 
206
  ```python
207
  class KV:
208
+ # Folder memory index — key = "{folder_path}:{agent_id}", value = FolderIndexEntry
209
  folders = "mem:folders"
210
 
211
+ # O(1) observation hydration index — key = obs_id, value = {folderPath, agentId}
212
+ obs_lookup = "mem:obs_lookup"
213
+
214
+ # Per-(folder, agent) observations — key = obs_id, value = FolderObservation
215
  @staticmethod
216
  def folder_obs(folder_path: str, agent_id: str) -> str:
217
  safe_path = folder_path.replace("\\", "/").strip("/")
218
  safe_agent = agent_id.strip()
219
  return f"mem:folder:{safe_path}:{safe_agent}"
220
 
221
+ # Per-(folder, agent) metadata key = "meta", value = FolderMeta
222
  @staticmethod
223
  def folder_meta(folder_path: str, agent_id: str) -> str:
224
  safe_path = folder_path.replace("\\", "/").strip("/")
 
228
  # Global long-term memories (unchanged)
229
  memories = "mem:memories"
230
 
231
+ # BM25 index shards (sharded, 2MB chunks)
232
  bm25Index = "mem:index:bm25"
233
 
234
+ # Audit log
235
  audit = "mem:audit"
236
+
237
+ # Graph edges (repurposed for folder graph)
238
+ relations = "mem:relations"
239
+
240
+ # Legacy scopes (read-only — for migration only)
241
+ sessions = "mem:sessions"
242
+
243
+ @staticmethod
244
+ def observations(session_id: str) -> str:
245
+ return f"mem:obs:{session_id}"
246
+
247
+ # Legacy (retained for backward compat code paths)
248
+ summaries = "mem:summaries"
249
+ profiles = "mem:profiles"
250
+ slots = "mem:slots"
251
+ imageRefs = "mem:image-refs"
252
+ globalSlots = "mem:global-slots"
253
  ```
254
 
255
+ **Notable addition**: `obs_lookup = "mem:obs_lookup"` an O(1) reverse-lookup index that maps `obs_id {folderPath, agentId}`, enabling fast hydration during search without scanning all folder scopes. Backfilled on startup via `backfill_obs_lookup_if_needed()`.
256
 
257
+ **Scopes fully removed from active use**: `lessons`, `actions`, `action-edges`, `crystals`, `sketches`, `facets`, `sentinels`, `signals`, `checkpoints`, `routines`, `routine-runs`, `semantic`, `procedural`, `commits`, `leases`, `mesh`, `recent-searches`, `claude-bridge`, `insights`, `retention`, `access`
258
 
259
  ---
260
 
 
863
 
864
  ---
865
 
866
+ ## MCP Tools: Implemented
867
+
868
+ The server exposes **12 MCP tools** via `GET /agentmemory/mcp/tools` and `POST /agentmemory/mcp/tools`.
869
 
870
+ ### Active Tools
871
 
872
+ | Tool | Description | Notes |
873
+ |------|-------------|-------|
874
+ | `agent_observe` | Log observation to a `(folderPath, agentId)` pair | Required: `folderPath`, `agentId`, `text`. Legacy `sessionId`/`cwd`/`project` accepted as fallback. |
875
+ | `agent_remember` | Save to global long-term memory | Unchanged |
876
+ | `memory_recall` | BM25+vector search across folder obs + global memories | Optional `folderPath`/`agentId` filters |
877
+ | `memory_smart_search` | Alias for `memory_recall` (hybrid search) | Same implementation |
878
+ | `memory_save` | Explicit save to global memory | Unchanged |
879
+ | `memory_export` | Export all data as JSON v2 format | `{folders: [...], memories: [...], version: "2.0"}` |
880
+ | `memory_forget` | Delete memory or folder pair | Accepts `memoryId` OR `folderPath+agentId` |
881
+ | `memory_diagnose` | Health check — folder/agent/obs/memory counts | Replaces session-based counts |
882
+ | `memory_folders` | List all `(folder, agent)` pairs | Returns sorted by `lastUpdated` |
883
+ | `memory_folder_observations` | Get observations for a specific pair | Required: `folderPath`, `agentId` |
884
+ | `memory_timeline` | Folder activity feed | Optional `folderPath`, `agentId`, `limit`, `before`, `after` |
885
 
886
+ ### Removed Tools (from original session-based model)
887
 
888
+ `memory_sessions`, `memory_sessions_list`, `memory_observations` (session-based), `memory_profile`, `memory_lessons`, `memory_lesson_save`, `memory_lesson_recall`, `memory_lesson_search`, `memory_consolidate`, `memory_reflect`, `memory_crystallize`, `memory_slot_list`, `memory_slot_get`, `memory_slot_create`, `memory_slot_append`, `memory_slot_replace`, `memory_slot_delete`, `memory_action_create`, `memory_action_update`, `memory_frontier`, `memory_antigravity_sync`, `memory_antigravity_sync_all`
 
 
 
 
889
 
890
+ ---
891
 
892
+ ## REST Endpoints: Implemented
893
+
894
+ ### Active Endpoints
895
+
896
+ | Method | Path | Blueprint | Description |
897
+ |--------|------|-----------|-------------|
898
+ | POST | `/agentmemory/observe` | observations | Legacy compat shim — maps to folder_observe |
899
+ | POST | `/agentmemory/agent/observe` | observations | Folder observation ingest (strict) |
900
+ | GET | `/agentmemory/folders` | observations | List all (folder, agent) pairs |
901
+ | GET | `/agentmemory/folder/observations` | observations | Observations for a pair |
902
+ | POST | `/agentmemory/session/start` | observations | Legacy compat no-op → returns synthetic session ID |
903
+ | POST | `/agentmemory/session/end` | observations | Legacy compat no-op |
904
+ | GET | `/agentmemory/observations` | observations | Legacy compat reads from `mem:obs:{sessionId}` |
905
+ | POST | `/agentmemory/remember` | memories | Save global memory |
906
+ | POST | `/agentmemory/agent/remember` | memories | Save global memory (agent-scoped variant) |
907
+ | GET | `/agentmemory/memories` | memories | List global memories |
908
+ | POST | `/agentmemory/forget` | memories | Delete memory / folder pair |
909
+ | POST | `/agentmemory/search` | search | BM25+vector hybrid search |
910
+ | POST | `/agentmemory/timeline` | search | Folder activity feed |
911
+ | GET | `/agentmemory/graph` | graph | Graph nodes+edges |
912
+ | GET | `/agentmemory/graph/stats` | graph | Node/edge count |
913
+ | POST | `/agentmemory/graph/query` | graph | Reserved (returns empty) |
914
+ | POST | `/agentmemory/graph/build` | graph | Trigger consolidation if enabled |
915
+ | GET | `/agentmemory/livez` | health | Liveness probe (no auth) |
916
+ | GET | `/agentmemory/health` | health | Full health check |
917
+ | GET | `/agentmemory/audit` | health | Audit log |
918
+ | GET | `/agentmemory/config/flags` | health | Feature flags + provider info |
919
+ | GET | `/agentmemory/mcp/tools` | mcp | MCP tool schemas |
920
+ | POST | `/agentmemory/mcp/tools` | mcp | MCP tool dispatch |
921
+ | POST | `/agentmemory/migrate` | migration | Session → folder migration |
922
 
923
  ---
924
 
.kiro/specs/folder-based-memory/tasks.md CHANGED
@@ -2,15 +2,14 @@
2
 
3
  ## Overview
4
 
5
- The folder-based memory migration is complete. This plan covers the next phase:
6
- structural refactoring to eliminate the god-file problem, UX uplift on the viewer
7
- dashboard, reliability improvements to the storage and indexing layers, and new
8
- developer-facing features. Work is organized into four parallel tracks that can be
9
- assigned to different engineers independently.
10
 
11
- **Track A — Backend Structure** (Senior Backend)
12
  **Track B — Viewer UX** (Frontend / Full-Stack)
13
- **Track C — Reliability & Testing** (Backend / QA)
14
  **Track D — Integrations & DX** (DevEx / Infra)
15
 
16
  ---
@@ -19,333 +18,158 @@ assigned to different engineers independently.
19
 
20
  ## Track A — Backend Structure Refactor
21
 
22
- > Owner: Senior Backend Dev
23
- > Goal: Break the god files apart so the codebase scales to parallel contributions.
24
- > Priority: CRITICAL — blocks all other backend work from being clean.
25
-
26
  - [x] A1. Split `src/app.py` into Flask blueprints
27
- - [x] A1.1 Create `src/routes/` directory with `__init__.py`
28
- - Move observation routes (`/observe`, `/agent/observe`, `/folder/observations`, `/folders`) → `src/routes/observations.py`
29
- - Move memory routes (`/remember`, `/agent/remember`, `/memories`, `/forget`) → `src/routes/memories.py`
30
- - Move search + timeline routes (`/search`, `/timeline`) → `src/routes/search.py`
31
- - Move graph routes (`/graph`, `/graph/stats`, `/graph/query`) `src/routes/graph.py`
32
- - Move health + audit routes (`/livez`, `/health`, `/audit`, `/config/flags`) → `src/routes/health.py`
33
- - Move MCP routes (`/mcp/tools` GET + POST) → `src/routes/mcp.py`
34
- - Move migration route (`/migrate`) → `src/routes/migration.py`
35
- - _Each blueprint file registers with `Blueprint(__name__)` and imports only what it needs_
36
 
37
  - [x] A1.2 Rebuild `src/app.py` as a thin factory
38
- - `create_app()` factory function: init DB, init embeddings, register all blueprints, set up WebSocket, register `after_request` CORS hook
39
- - Keep `init_app()` logic but move background thread setup into a separate `src/workers.py`
40
- - `if __name__ == "__main__"` block stays minimal — just `create_app().run()`
41
- - Target: `app.py` under 150 lines after refactor
42
 
43
  - [x] A1.3 Validate no route regressions after blueprint split
44
- - Run full `pytest tests/` suite; manually hit every endpoint via `/livez` sanity pass
45
- - Check WebSocket still connects and broadcasts after split
46
-
47
- - [ ] A2. Split `src/functions.py` into focused modules
48
- - [ ] A2.1 Create `src/memory/` package
49
- - `src/memory/observe.py` — `folder_observe()`, `observe()`, `build_synthetic_compression()`, `strip_private_data()`
50
- - `src/memory/remember.py` — `remember()`, `forget()`, `jaccard_similarity()`
51
- - `src/memory/context.py` — `context()`, `export_data()`, `rebuild_index()`
52
- - `src/memory/graph.py` — `folder_graph_build()`, `get_relations()`, `add_relation()`, `folderColor()`
53
- - `src/memory/timeline.py` — `folder_timeline()`, `folder_search()`
54
- - `src/memory/health.py` `health_check()`, `auto_forget()`
55
 
56
  - [~] A2.2 Keep `src/functions.py` as a compatibility shim
57
- - Re-export everything from `src/memory/*` via `from memory.observe import *` etc.
58
- - This avoids breaking `app.py` imports during the transition period
59
- - Remove the shim in a follow-up PR once blueprints are fully referencing the new modules
60
 
61
- - [~] A2.3 Move `KV` class and path utilities to `src/storage/`
62
- - `src/storage/scopes.py` — `KV` class with all scope definitions
63
  - `src/storage/paths.py` — `normalize_folder_path()`, `validate_agent_id()`, `generate_id()`, `fingerprint_id()`
64
  - `src/storage/images.py` — `save_image_to_disk()`, `delete_image()`, `touch_image()`, `is_managed_image_path()`
65
 
66
- - [ ] A3. Fix `src/db.py` connection management
67
- - [~] A3.1 Implement per-thread persistent connections using `threading.local()`
68
- - Replace every `conn = self._get_conn()` / `conn.close()` pattern with a `_local = threading.local()` cache
69
- - Connection is opened once per thread and reused; close only on explicit `teardown()` or thread exit
70
- - Keep the write `_lock` for serializing INSERT/UPDATE/DELETE operations
71
-
72
- - [~] A3.2 Add WAL checkpoint on graceful shutdown
73
- - Register a `SIGTERM` / `atexit` handler in `src/workers.py` that calls `PRAGMA wal_checkpoint(FULL)` before exit
74
- - This ensures the BM25/vector index shards are flushed to the DB before the process dies
75
-
76
- - [~] A3.3 Add `db.stats()` method for the health endpoint
77
- - Return `{"db_size_bytes": ..., "kv_row_count": ..., "audit_row_count": ..., "wal_size_bytes": ...}`
78
- - Used by the upgraded `/health` endpoint
79
-
80
- - [ ] A4. Debounce index persistence writes
81
- - [~] A4.1 Replace `IndexPersistence.schedule_save()` with a real debounce queue
82
- - Use a `threading.Timer` that resets on each call; fires the actual `save()` after 5 seconds of inactivity
83
- - This prevents a save on every single observation write under high throughput
84
 
85
- - [~] A4.2 Add dirty-flag tracking to `SearchIndex` and `VectorIndex`
86
- - Set `self._dirty = True` in `add()` and `remove()`; reset in `save()`
87
- - Skip `save()` entirely if `not _dirty`
88
-
89
- - [~] A4.3 Write unit test for debounce behavior
90
- - Assert that 100 rapid `add()` calls result in exactly 1 persistence save, not 100
91
 
92
  ---
93
 
94
  ## Track B — Viewer UX Overhaul
95
 
96
  > Owner: Frontend / Full-Stack Dev
97
- > Goal: Elevate the viewer from a debug tool to a real developer dashboard.
98
- > Priority: HIGH — highest user-visible impact.
99
 
100
  - [ ] B1. Add Command Palette (`Ctrl+K` / `Cmd+K`)
101
- - [~] B1.1 Build command palette overlay in `index.html`
102
- - Fuzzy-search across: folder names, memory content snippets, observation titles, tab names
103
- - Data sources: in-memory cache of last-loaded folders + memories (no extra API calls on keypress)
104
- - Keyboard: `↑`/`↓` to navigate results, `Enter` to navigate to item, `Escape` to close
105
- - Show result type badge (Folder / Memory / Observation) and secondary detail (agentId / timestamp)
106
-
107
- - [~] B1.2 Register global keydown listener
108
- - `document.addEventListener("keydown", ...)` — detect `Ctrl+K` on Windows/Linux, `Cmd+K` on Mac
109
- - Focus input field on open; re-focus last active element on close
110
-
111
- - [~] B1.3 Style command palette
112
- - Modal overlay with `backdrop-filter: blur(4px)` behind the panel
113
- - Max-width 640px, centered; input at top, results list below with keyboard-highlight state
114
- - Respect `data-theme="dark"` CSS vars; animate open/close with `transform: translateY(-8px) → 0` + `opacity`
115
 
116
  - [ ] B2. Replace Canvas graph with D3 force-directed SVG
117
- - [~] B2.1 Add D3 v7 via CDN with CSP nonce
118
- - Load `d3.v7.min.js` via `<script nonce="__AGENTMEMORY_VIEWER_NONCE__">` inline or from self-hosted path
119
- - Remove the existing Canvas `<canvas>` element and hand-rolled physics loop
120
-
121
- - [~] B2.2 Implement D3 force simulation on SVG
122
- - Nodes as `<circle>` + `<text>` SVG groups; size proportional to `obsCount`
123
- - Three link types rendered as distinct path styles: `same-parent` (dashed `stroke-dasharray: 6,3`), `cross-ref` (solid), `agent-shared` (dotted `stroke-dasharray: 2,4`)
124
- - `d3.forceManyBody().strength()` tuned per node count: `-80` for < 20 nodes, `-200` for 20–100, `-400` for 100+
125
-
126
- - [~] B2.3 Add zoom/pan and click-to-expand
127
- - `d3.zoom()` for pan + scroll-to-zoom; double-click node to highlight its edges and neighbors, dim all others
128
- - Click on a node opens the Folders tab filtered to that `folderPath`
129
-
130
- - [~] B2.4 Fix graph edge label overlap bug (ROADMAP item)
131
- - Use `d3.forceCollide()` on label bounding boxes; set `textAnchor` dynamically based on node position relative to center
132
- - Labels only show at zoom > 0.6; below that, show labels only for hovered node
133
 
134
  - [ ] B3. Implement virtual scroll on Timeline and Folders tabs
135
- - [~] B3.1 Build a reusable `VirtualList` helper in JS
136
- - Takes `container`, `items[]`, `rowHeight`, `renderItem(item) → HTMLElement`
137
- - Uses `IntersectionObserver` on a sentinel element at the bottom to load next page
138
- - Maintains a window of ~50 rendered DOM nodes, recycling off-screen rows
139
-
140
- - [~] B3.2 Apply `VirtualList` to Timeline tab
141
- - Replace current `observations.forEach(renderObsCard)` with `VirtualList`
142
- - Fetch first 50 observations; load next 50 on scroll near bottom via `/timeline` with `before` cursor from last item's timestamp
143
-
144
- - [~] B3.3 Apply `VirtualList` to Folders tab observation drill-down
145
- - When a folder row is expanded, load observations with the same virtual scroll pattern
146
 
147
  - [ ] B4. First-run onboarding experience
148
- - [~] B4.1 Detect empty state on Dashboard tab load
149
- - Check `health_check` response: if `observationCount === 0` and `memoryCount === 0`, show onboarding card
150
-
151
- - [~] B4.2 Build onboarding card component
152
- - Three-step guide: (1) Configure `AGENTMEMORY_SECRET` env var, (2) Set `GEMINI_API_KEY` for semantic search, (3) Point your agent hook at `POST /agentmemory/agent/observe`
153
- - Each step shows a code snippet (pre-formatted, with a copy button) and a green checkmark if the feature is already active (read from `/config/flags` response)
154
- - Dismissable with "Got it" button; stores dismissed state in `localStorage`
155
-
156
- - [~] B4.3 Add "copy to clipboard" utility function
157
- - `copyToClipboard(text)` uses `navigator.clipboard.writeText()` with fallback to `document.execCommand("copy")`
158
- - Show a transient "Copied!" tooltip for 1.5 seconds after click
159
 
160
  - [ ] B5. Add MCP Tool Tester tab
161
- - [~] B5.1 Add "Tools" tab to the tab bar
162
- - Fetch `GET /agentmemory/mcp/tools` on tab activate; render a list of all tools with name + description
163
-
164
- - [~] B5.2 Build tool invocation panel
165
- - Click a tool → show its `inputSchema` as an auto-generated JSON editor (textarea pre-filled with `{}` containing all required keys)
166
- - "Run" button posts to `POST /agentmemory/mcp/tools` with `{name, arguments}`
167
- - Response rendered as syntax-highlighted JSON below the editor; error state shown in red
168
-
169
- - [~] B5.3 Add request/response history for the session
170
- - Keep last 10 tool calls in memory; render as collapsible history entries above the editor
171
- - Each history entry shows tool name, timestamp, and success/error badge
172
 
173
  - [ ] B6. Viewer polish and accessibility
174
- - [~] B6.1 Fix dark mode inconsistencies
175
- - Audit all components against `data-theme="dark"` CSS vars; find and fix the graph tooltip and graph controls that don't fully respond to theme changes
176
- - Test in both themes: all text must meet WCAG AA contrast ratio (4.5:1 for body, 3:1 for large text)
177
-
178
- - [~] B6.2 Add keyboard navigation for tabs
179
- - `Tab` key cycles through tab buttons; `Enter`/`Space` activates; `←`/`→` arrow keys move between tabs when a tab has focus
180
- - Add `role="tablist"`, `role="tab"`, `aria-selected`, `aria-controls` ARIA attributes to the tab bar
181
-
182
- - [~] B6.3 Add loading skeleton states
183
- - Replace `<div class="loading">Loading...</div>` with animated skeleton placeholder cards that match the shape of the real content
184
- - Use CSS `@keyframes shimmer` with a `background: linear-gradient(90deg, ...)` animation
185
 
186
  ---
187
 
188
  ## Track C — Reliability & Testing
189
 
190
- > Owner: Backend / QA Dev
191
- > Goal: Close the test coverage gap and harden the system against bad inputs and restarts.
192
- > Priority: HIGH required before any production deployment.
193
-
194
- - [ ] C1. Write tests for the core observe/remember/search/context pipeline
195
- - [~] C1.1 Create `tests/test_observe_core.py`
196
- - Test `observe()` with valid payload → returns obs dict with correct fields
197
- - Test `observe()` with missing `sessionId` / `hookType` / `timestamp` → raises `ValueError`
198
- - Test that `strip_private_data()` fires and `[REDACTED_SECRET]` appears in stored obs
199
- - Test that `MAX_OBS_PER_SESSION` env cap raises `ValueError` when exceeded
200
-
201
- - [~] C1.2 Create `tests/test_remember.py`
202
- - Test `remember()` creates a new memory with `isLatest=True`
203
- - Test that a second `remember()` with > 0.7 Jaccard similarity sets `isLatest=False` on the old memory and links via `parentId`
204
- - Test that a second `remember()` with < 0.7 similarity creates an independent second memory
205
-
206
- - [~] C1.3 Create `tests/test_search.py`
207
- - Test `SearchIndex.add()` + `SearchIndex.search()` returns the added document at rank 1 for an exact-match query
208
- - Test prefix matching: index `"authentication"`, search `"authen"` → doc appears in results
209
- - Test synonym expansion: index `"database connection"`, search `"db conn"` → doc appears via synonym map
210
- - Test `HybridSearch.search()` in BM25-only mode (no embedding provider) returns same results as `SearchIndex.search()`
211
-
212
- - [~] C1.4 Create `tests/test_context.py`
213
- - Test `context()` respects `TOKEN_BUDGET` — returned context string is under budget
214
- - Test that pinned slot content appears first in context output
215
- - Test that empty DB returns a well-formed but minimal context dict
216
-
217
- - [ ] C2. Write Hypothesis property tests
218
- - [~] C2.1 Create `tests/test_properties.py` with all 8 property tests
219
- - **Property 1: Pair Isolation** two distinct `(folderPath, agentId)` pairs never share observations
220
- - **Property 2: Observation Count Consistency** — `meta.obsCount == len(kv.list(folder_obs_scope))`
221
- - **Property 3: Index Coverage** every written pair has a `KV.folders` entry
222
- - **Property 4: Privacy Invariant** no stored obs text matches any secret regex pattern after `folder_observe()`
223
- - **Property 5: Timeline Ordering** — `folder_timeline()` always returns results sorted newest-first
224
- - **Property 6: Forget Completeness** after `forget({folderPath, agentId})`, all three scopes are empty and BM25 has no entry for any obs from that pair
225
- - **Property 7: Memory Version Uniqueness** — only one memory per concept cluster has `isLatest=True`
226
- - **Property 8: Path Normalization Idempotency** — `normalize(normalize(p)) == normalize(p)` for all valid inputs
227
- - Use `tmp_path` pytest fixture for isolated SQLite DB per test; `@settings(max_examples=50)`
228
-
229
- - [ ] C3. Add integration tests for REST endpoints
230
- - [~] C3.1 Create `tests/test_api.py` using Flask test client
231
- - Test `POST /agentmemory/agent/observe` → 201 with obs id
232
- - Test `POST /agentmemory/agent/observe` missing `folderPath` → 400
233
- - Test `POST /agentmemory/search` with query → 200 with results list
234
- - Test `GET /agentmemory/folders` → 200 with folders list
235
- - Test `GET /agentmemory/health` → 200 with `folderCount`, `observationCount` keys
236
- - Test `GET /agentmemory/livez` → 200 without auth (open endpoint)
237
- - Test authenticated endpoints return 401 when `AGENTMEMORY_SECRET` is set but token is wrong
238
-
239
- - [ ] C4. Improve HuggingFace sync reliability
240
  - [~] C4.1 Replace mtime-based sync trigger with audit log high-water mark
241
- - On each sync iteration, read `MAX(ts)` from `audit_log`; compare to last-synced high-water mark stored in a local `.sync_state` file
242
- - Only upload if `current_max_ts > last_synced_ts`; update the watermark after successful upload
243
- - This eliminates false-positive uploads caused by WAL mode mtime changes
244
 
245
- - [~] C4.2 Add sync status to the `/health` endpoint
246
- - Report `last_sync_at` (ISO timestamp of last successful HF upload), `sync_status` (`"ok"` / `"never"` / `"error"`), and `db_size_bytes`
247
- - Read from `.sync_state` file; if file doesn't exist, report `"never"`
248
-
249
- - [ ] C5. Add graceful shutdown handler
250
- - [~] C5.1 Register `SIGTERM` and `SIGINT` handlers in `src/workers.py`
251
- - On signal: set a global `_shutting_down` flag; wait for any in-flight `schedule_save()` debounce timer to fire; call `persistence.save()` synchronously; log shutdown message
252
- - Exit cleanly with code 0
253
 
254
  ---
255
 
256
  ## Track D — Integrations & Developer Experience
257
 
258
- > Owner: DevEx / Infra Dev
259
- > Goal: Make agentmemory easier to install, configure, and connect to agents.
260
- > Priority: MEDIUM — high value but not blocking.
261
-
262
  - [ ] D1. Clean up dead code and scratch files
263
- - [~] D1.1 Delete scratch and personal utility files
264
- - Remove `scratch_diff.txt`, `scratch_test_import.py` from repo root
265
- - Remove `push_local_data.py` and `push_second_brain.py` — these are personal sync scripts, not library code
266
- - Remove `migrate_dolt_to_sqlite.py` the migration is done; archive it in `docs/migration/` if it needs to be kept for reference
267
 
268
- - [~] D1.2 Remove all 410-stub route handlers from `app.py`
269
- - Delete route functions for: `api_session_start`, `api_session_end`, `api_session_commit`, `api_session_by_commit`, `api_sessions`, `api_replay_sessions`, `api_antigravity_sync`, `api_lessons_list`, `api_lessons_save`, `api_lessons_search`, `api_lessons_strengthen`, `api_slots_list`, `api_slots_get`, `api_slots_create`, `api_slots_append`, `api_slots_replace`, `api_slots_delete`, `api_slots_reflect`, `api_timeline_legacy`
270
- - These return 410 and serve no purpose except confusion; the routes are already documented as removed
271
 
272
- - [~] D1.3 Fix dead references in `mcp_stdio.py`
273
- - `perform_antigravity_sync_all_local()` calls `/summarize` and `/slot/reflect` which are 410 update or remove these calls
274
- - The Antigravity sync logic (both functions) is personal/business-specific; move to `examples/antigravity_sync.py` and remove from the generic MCP bridge
275
- - Clean up the double-import of `os`, `json`, `requests` inside `perform_antigravity_sync_local()`
276
 
277
- - [~] D1.4 Move `DESIGN.md` and clarify its purpose
278
- - Rename to `docs/viewer-design-system.md` the current name `DESIGN.md` implies system architecture
279
- - Create a real `ARCHITECTURE.md` at the repo root that documents the Flask blueprint structure, module responsibilities, KV scope layout, and data flow (observe → BM25/vector → WebSocket → viewer)
 
280
 
281
- - [ ] D2. Fix CORS and security hardening
282
- - [~] D2.1 Move CORS allowlist to configuration
283
- - Replace the `after_request` string-matching logic with a configurable `AGENTMEMORY_CORS_ORIGINS` env var (comma-separated)
284
- - Default to `http://localhost:*,http://127.0.0.1:*,vscode-webview://*` — still localhost-only but driven by config
285
- - Add `chrome-extension://` support as an explicit opt-in via env var, not hardcoded
286
-
287
- - [~] D2.2 Stop injecting `AGENTMEMORY_SECRET` into HTML template
288
- - The current `__AGENTMEMORY_AUTO_TOKEN__` replacement embeds the raw secret in the page source
289
- - Replace with a session-scoped viewer token: on page load, the server issues a short-lived viewer token (signed HMAC of `secret + timestamp`, valid 1h) via a `Set-Cookie` header
290
- - The viewer uses this cookie for API calls; the actual `AGENTMEMORY_SECRET` never touches the DOM
291
-
292
- - [ ] D3. Build prebuilt agent hook scripts
293
- - [~] D3.1 Create `hooks/claude-code-hook.sh` for Claude Code
294
- - Bash script that posts to `POST /agentmemory/agent/observe` on each tool use
295
- - Reads `AGENTMEMORY_URL` and `AGENTMEMORY_SECRET` from environment
296
- - Include `folderPath` from `$PWD`, `agentId` from `$CLAUDE_AGENT_ID` or `"claude-code"`
297
-
298
- - [~] D3.2 Create `hooks/cursor-hook.js` for Cursor
299
- - JavaScript snippet for Cursor's `.cursorrules` hook system
300
- - Same endpoint and payload structure as the bash version
301
-
302
- - [~] D3.3 Create `hooks/powershell-hook.ps1` for terminal sessions
303
- - Formalize and generalize the hook currently documented in `agentmemory.md`
304
- - Parameterize `$ServerUrl` and `$AgentId`; ship as a ready-to-source snippet
305
-
306
- - [~] D3.4 Update `INSTALL_FOR_AGENTS.md` with hook setup instructions
307
- - One section per agent type; link to the corresponding hook script in `hooks/`
308
- - Include the `.env` file format and `AGENTMEMORY_SECRET` setup steps
309
-
310
- - [ ] D4. Build `pip`-installable package
311
- - [~] D4.1 Add `pyproject.toml`
312
- - Package name: `agentmemory`; entry point: `agentmemory = src.app:main`
313
- - Include `src/viewer/` as package data
314
- - Python `>=3.10` requirement
315
-
316
- - [~] D4.2 Add CLI entrypoint
317
- - `agentmemory serve [--port PORT] [--host HOST]` — starts the Flask server
318
- - `agentmemory migrate` — runs `migrate_sessions_to_folders()` and prints summary
319
- - `agentmemory export [--output FILE]` — runs `export_data()` and writes JSON
320
-
321
- - [~] D4.3 Update GitHub Actions CI workflow
322
- - Add steps: `pip install -e .[dev]`, `pytest tests/ -v`, `pip install build; python -m build`
323
- - Run on `push` to `main` and `pull_request`
324
-
325
- - [ ] D5. Additional embedding provider support
326
- - [~] D5.1 Add `OpenAIEmbeddingProvider` class to `src/search.py`
327
- - Mirror the `GeminiEmbeddingProvider` interface: `embed(text)`, `embed_batch(texts)`, `dimensions` (1536 for `text-embedding-3-small`)
328
- - Use `urllib.request` (no new dependencies); read API key from `OPENAI_API_KEY` env var
329
-
330
- - [~] D5.2 Add local `sentence-transformers` provider (optional install)
331
- - `SentenceTransformerProvider` class; lazy-import `sentence_transformers`; log a clear error if not installed
332
- - Default model: `all-MiniLM-L6-v2` (384 dims); configurable via `AGENTMEMORY_LOCAL_EMBEDDING_MODEL` env var
333
-
334
- - [~] D5.3 Auto-select embedding provider in `init_app()`
335
- - Priority: `GEMINI_API_KEY` → `OPENAI_API_KEY` → `AGENTMEMORY_LOCAL_EMBEDDING_MODEL` → BM25-only
336
- - Log which provider was selected on startup
337
 
338
  ---
339
 
340
- ## Notes
341
 
342
- - **Track A must be completed before any other backend feature work.** The blueprint split is the prerequisite that makes parallel development safe. Do not add new routes to the monolithic `app.py`.
343
- - **Track B and Track C can run in parallel** with Track A. The viewer and test suite touch different files.
344
- - **Track D tasks D1.1–D1.3 (cleanup) can be done immediately** — they are deletions and do not require Track A to finish first.
345
- - All new routes follow the existing auth pattern: call `check_auth()` at the top; never skip it.
346
- - All new viewer JS follows the existing CSP constraint: no inline event handlers, all scripts nonce-tagged.
347
- - Property tests in `tests/test_properties.py` (C2.1) are the authoritative source; unit tests in C1 are complementary, not replacements.
348
- - The `D2.2` security fix (token injection) should be shipped before any public/shared deployment.
349
 
350
  ---
351
 
@@ -356,33 +180,28 @@ assigned to different engineers independently.
356
  "waves": [
357
  {
358
  "id": 0,
359
- "label": "Cleanup (no dependencies)",
360
- "tasks": ["D1.1", "D1.2", "D1.3", "D1.4"]
361
  },
362
  {
363
  "id": 1,
364
- "label": "Structural foundation",
365
- "tasks": ["A1.1", "A2.3", "A3.1", "C1.1", "C1.2", "C1.3", "B4.1", "B4.2"]
366
  },
367
  {
368
  "id": 2,
369
- "label": "Module split + UX foundation",
370
- "tasks": ["A1.2", "A2.1", "A2.2", "A3.2", "A4.1", "B1.1", "B2.1", "B3.1", "C1.4", "D2.1"]
371
  },
372
  {
373
  "id": 3,
374
- "label": "Blueprint validation + UX components",
375
- "tasks": ["A1.3", "A3.3", "A4.2", "A4.3", "B1.2", "B1.3", "B2.2", "B3.2", "B3.3", "C2.1", "C3.1", "D2.2"]
376
  },
377
  {
378
  "id": 4,
379
- "label": "Advanced features",
380
- "tasks": ["B2.3", "B2.4", "B4.3", "B5.1", "B5.2", "C4.1", "C4.2", "C5.1", "D3.1", "D3.2", "D3.3", "D5.1"]
381
- },
382
- {
383
- "id": 5,
384
  "label": "Polish + packaging",
385
- "tasks": ["B5.3", "B6.1", "B6.2", "B6.3", "D3.4", "D4.1", "D4.2", "D4.3", "D5.2", "D5.3"]
386
  }
387
  ]
388
  }
 
2
 
3
  ## Overview
4
 
5
+ The folder-based memory migration is complete. The backend structural refactor (Track A)
6
+ is largely done blueprints are split, `src/memory/` and `src/storage/` packages exist,
7
+ `db.py` has per-thread connections + WAL checkpoint + stats, and `IndexPersistence` has
8
+ debounce + dirty-flag tracking. This plan tracks what remains.
 
9
 
10
+ **Track A — Backend Structure** mostly complete
11
  **Track B — Viewer UX** (Frontend / Full-Stack)
12
+ **Track C — Reliability & Testing** largely complete
13
  **Track D — Integrations & DX** (DevEx / Infra)
14
 
15
  ---
 
18
 
19
  ## Track A — Backend Structure Refactor
20
 
 
 
 
 
21
  - [x] A1. Split `src/app.py` into Flask blueprints
22
+ - [x] A1.1 Create `src/routes/` directory with blueprints
23
+ - `src/routes/observations.py` `/observe`, `/agent/observe`, `/folder/observations`, `/folders`
24
+ - `src/routes/memories.py` `/remember`, `/agent/remember`, `/memories`, `/forget`
25
+ - `src/routes/search.py` `/search`, `/timeline`
26
+ - `src/routes/graph.py` `/graph`, `/graph/stats`, `/graph/query`, `/graph/build`
27
+ - `src/routes/health.py` `/livez`, `/health`, `/audit`, `/config/flags`
28
+ - `src/routes/mcp.py` `/mcp/tools` GET + POST
29
+ - `src/routes/migration.py` — `/migrate`
 
30
 
31
  - [x] A1.2 Rebuild `src/app.py` as a thin factory
32
+ - `create_app()` factory: init DB, init embeddings (D5.3 priority: Gemini → OpenAI → SentenceTransformer → BM25-only), register all blueprints, set up WebSocket, register CORS hook
33
+ - Background worker setup moved to `src/workers.py`
34
+ - `app.py` is now ~170 lines
 
35
 
36
  - [x] A1.3 Validate no route regressions after blueprint split
37
+ - `tests/test_route_regressions.py` covers all main endpoints
38
+
39
+ - [x] A2. Split `src/functions.py` into focused modules
40
+ - [x] A2.1 Create `src/memory/` package
41
+ - `src/memory/observe.py` — thin re-export shim for `folder_observe`, `observe`, `build_synthetic_compression`, `strip_private_data`
42
+ - `src/memory/remember.py` — thin re-export shim for `remember`, `forget`, `jaccard_similarity`
43
+ - `src/memory/context.py` — thin re-export shim for `context`, `export_data`, `rebuild_index`
44
+ - `src/memory/graph.py` — thin re-export shim for `folder_graph_build`
45
+ - `src/memory/timeline.py` — thin re-export shim for `folder_timeline`, `folder_search`
46
+ - `src/memory/health.py` — thin re-export shim for `health_check`, `auto_forget`
47
+ - _Note: the shims currently delegate back to `functions.py`. The canonical implementation still lives in `functions.py`._
48
 
49
  - [~] A2.2 Keep `src/functions.py` as a compatibility shim
50
+ - All routes import from `functions` directly; `src/memory/*` shims exist for future decoupling
 
 
51
 
52
+ - [x] A2.3 Move `KV` class and path utilities to `src/storage/`
53
+ - `src/storage/scopes.py` — `KV` class (copy; canonical version still in `functions.py`)
54
  - `src/storage/paths.py` — `normalize_folder_path()`, `validate_agent_id()`, `generate_id()`, `fingerprint_id()`
55
  - `src/storage/images.py` — `save_image_to_disk()`, `delete_image()`, `touch_image()`, `is_managed_image_path()`
56
 
57
+ - [x] A3. Fix `src/db.py` connection management
58
+ - [x] A3.1 Per-thread persistent connections via `threading.local()`
59
+ - [x] A3.2 WAL checkpoint on graceful shutdown (`atexit` + `SIGTERM`/`SIGINT` handlers in `workers.py`)
60
+ - [x] A3.3 `db.stats()` method returning `db_size_bytes`, `kv_row_count`, `audit_row_count`, `wal_size_bytes`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
+ - [x] A4. Debounce index persistence writes
63
+ - [x] A4.1 `IndexPersistence.schedule_save()` uses `threading.Timer` (5s debounce)
64
+ - [x] A4.2 `_dirty` flag on `SearchIndex` and `VectorIndex` — skip save if not dirty
65
+ - [~] A4.3 Unit test for debounce behavior → `tests/test_debounce.py` exists
 
 
66
 
67
  ---
68
 
69
  ## Track B — Viewer UX Overhaul
70
 
71
  > Owner: Frontend / Full-Stack Dev
 
 
72
 
73
  - [ ] B1. Add Command Palette (`Ctrl+K` / `Cmd+K`)
74
+ - [~] B1.1–B1.3 Not yet implemented in `src/viewer/index.html`
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
  - [ ] B2. Replace Canvas graph with D3 force-directed SVG
77
+ - [~] B2.1–B2.4 Not yet implemented
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
  - [ ] B3. Implement virtual scroll on Timeline and Folders tabs
80
+ - [~] B3.1–B3.3 Not yet implemented
 
 
 
 
 
 
 
 
 
 
81
 
82
  - [ ] B4. First-run onboarding experience
83
+ - [~] B4.1–B4.3 Not yet implemented
 
 
 
 
 
 
 
 
 
 
84
 
85
  - [ ] B5. Add MCP Tool Tester tab
86
+ - [~] B5.1–B5.3 Not yet implemented
 
 
 
 
 
 
 
 
 
 
87
 
88
  - [ ] B6. Viewer polish and accessibility
89
+ - [~] B6.1–B6.3 Not yet implemented
 
 
 
 
 
 
 
 
 
 
90
 
91
  ---
92
 
93
  ## Track C — Reliability & Testing
94
 
95
+ > All test files exist and are passing.
96
+
97
+ - [x] C1. Core pipeline tests
98
+ - [x] `tests/test_observe_core.py` — folder observe validation, privacy stripping, cap enforcement
99
+ - [x] `tests/test_remember.py` memory creation, Jaccard dedup, independent memories
100
+ - [x] `tests/test_search.py` — BM25 add/search, prefix matching, synonym expansion, hybrid search
101
+ - [x] `tests/test_context.py` TOKEN_BUDGET, context compilation
102
+
103
+ - [x] C2. Hypothesis property tests
104
+ - [x] `tests/test_properties.py` 8 properties:
105
+ 1. Pair Isolation
106
+ 2. Observation Count Consistency
107
+ 3. Index Coverage
108
+ 4. Privacy Invariant
109
+ 5. Timeline Ordering
110
+ 6. Forget Completeness
111
+ 7. Memory Version Uniqueness
112
+ 8. Path Normalization Idempotency
113
+
114
+ - [x] C3. Integration tests
115
+ - [x] `tests/test_api.py` Flask test client hitting all main endpoints
116
+ - [x] `tests/test_route_regressions.py` — regression suite after blueprint split
117
+
118
+ - [x] Additional test files
119
+ - [x] `tests/test_folder_observe.py` folder_observe unit tests
120
+ - [x] `tests/test_folder_graph_build.py` graph builder tests
121
+ - [x] `tests/test_forget.py` — forget function tests
122
+ - [x] `tests/test_normalize.py` path normalization tests
123
+ - [x] `tests/test_obs_lookup.py` obs_lookup index tests
124
+ - [x] `tests/test_migration.py`session folder migration tests
125
+ - [x] `tests/test_timeline.py` folder_timeline tests
126
+ - [x] `tests/test_graph.py`graph edge tests
127
+ - [x] `tests/test_debounce.py`IndexPersistence debounce test
128
+
129
+ - [ ] C4. HuggingFace sync reliability
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  - [~] C4.1 Replace mtime-based sync trigger with audit log high-water mark
131
+ - [~] C4.2 Add sync status to `/health` endpoint
 
 
132
 
133
+ - [x] C5. Graceful shutdown handler
134
+ - [x] `src/workers.py` registers SIGTERM/SIGINT handlers, flushes persistence, runs WAL checkpoint
 
 
 
 
 
 
135
 
136
  ---
137
 
138
  ## Track D — Integrations & Developer Experience
139
 
 
 
 
 
140
  - [ ] D1. Clean up dead code and scratch files
141
+ - [~] D1.1 Remove scratch files (scratch_diff.txt, push_local_data.py, push_second_brain.py, migrate_dolt_to_sqlite.py)
142
+ - [~] D1.2 Remove 410-stub route handlers from app.py
143
+ - [~] D1.3 Fix dead references in `mcp_stdio.py`
144
+ - [~] D1.4 Move/rename `DESIGN.md` `docs/viewer-design-system.md`; ensure `ARCHITECTURE.md` is accurate
145
 
146
+ - [ ] D2. Fix CORS and security hardening
147
+ - [x] D2.1 CORS configurable via `AGENTMEMORY_CORS_ORIGINS` env var (implemented in `app.py`)
148
+ - [~] D2.2 Stop injecting `AGENTMEMORY_SECRET` into HTML template (viewer token via cookie)
149
 
150
+ - [ ] D3. Prebuilt agent hook scripts
151
+ - [~] D3.1–D3.4 `hooks/` directory may contain scripts; need verification and docs update
 
 
152
 
153
+ - [x] D4. `pip`-installable package
154
+ - [x] `pyproject.toml` with `agentmemory` entry point `src.cli:main`
155
+ - [x] `src/cli.py` CLI entrypoint exists
156
+ - [~] D4.3 GitHub Actions CI workflow updates
157
 
158
+ - [x] D5. Additional embedding providers
159
+ - [x] `OpenAIEmbeddingProvider` in `src/search.py` (D5.1)
160
+ - [x] `SentenceTransformerProvider` in `src/search.py` (D5.2)
161
+ - [x] Auto-select provider in `create_app()` — Gemini OpenAI SentenceTransformer → BM25-only (D5.3)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
 
163
  ---
164
 
165
+ ## Remaining High-Priority Work
166
 
167
+ The following items are not yet done and represent the bulk of remaining effort:
168
+
169
+ 1. **Track B (Viewer UX)** — Command palette, D3 graph, virtual scroll, onboarding, MCP tool tester, accessibility polish
170
+ 2. **D1.2** Remove 410 stub routes from `app.py` (they live in `observations.py` as compat shims — keep those, but the dead `app.py` stubs should be gone; already done since the monolith was split)
171
+ 3. **D2.2** Viewer token security (secret not in DOM)
172
+ 4. **C4** HF sync reliability with audit watermark
 
173
 
174
  ---
175
 
 
180
  "waves": [
181
  {
182
  "id": 0,
183
+ "label": "Completed structural foundation",
184
+ "tasks": ["A1.1", "A1.2", "A1.3", "A2.1", "A2.3", "A3.1", "A3.2", "A3.3", "A4.1", "A4.2", "C1.1", "C1.2", "C1.3", "C1.4", "C2.1", "C3.1", "C5.1", "D4.1", "D4.2", "D5.1", "D5.2", "D5.3", "D2.1"]
185
  },
186
  {
187
  "id": 1,
188
+ "label": "Next — viewer UX foundation",
189
+ "tasks": ["B4.1", "B4.2", "B4.3", "B1.1", "B2.1", "B3.1"]
190
  },
191
  {
192
  "id": 2,
193
+ "label": "Viewer components",
194
+ "tasks": ["B1.2", "B1.3", "B2.2", "B3.2", "B3.3", "B5.1", "B5.2", "D2.2"]
195
  },
196
  {
197
  "id": 3,
198
+ "label": "Advanced viewer + sync",
199
+ "tasks": ["B2.3", "B2.4", "B5.3", "B6.1", "B6.2", "B6.3", "C4.1", "C4.2"]
200
  },
201
  {
202
  "id": 4,
 
 
 
 
 
203
  "label": "Polish + packaging",
204
+ "tasks": ["D1.1", "D1.2", "D1.3", "D1.4", "D3.1", "D3.2", "D3.3", "D3.4", "D4.3"]
205
  }
206
  ]
207
  }
.windsurfrules CHANGED
@@ -1,20 +1,23 @@
1
- # Agent Memory Rules
2
 
3
- This workspace is integrated with long-term semantic memory via `agentmemory-python`.
4
- You must act as your own memory manager by calling the memory MCP tools at critical boundaries.
5
 
6
  ## Rules & Workflow
7
 
8
  1. **Initial Search (Prefetch Context)**:
9
- At the start of every session or new task, immediately call `memory_smart_search` with terms related to the current objective. This retrieves past architecture patterns, preferences, bug fixes, or lessons.
10
- - Example: `memory_smart_search(query="jwt token rotation logic")`
11
 
12
- 2. **Lessons & Insights Capture**:
 
 
 
 
13
  When you successfully debug a complex error, discover an undocumented requirement, or establish a convention, persist it:
14
- - Call `memory_lesson_save` to record lessons that improve your coding capabilities. Duplicate saves strengthen confidence scores.
15
- - Call `memory_save` to save long-term structural facts. Always extract 2-5 specific lowercased tags (e.g. `auth-flow`, `refresh-token`) as concepts.
16
 
17
- 3. **Checklist Before Ending**:
18
  Before stating a task is complete:
19
- - Reflect on whether any lessons learned should be saved.
20
- - Call `memory_reflect` to automatically distribute observations into slots if needed.
 
1
+ # Agent Cache Rules
2
 
3
+ This workspace is integrated with long-term semantic memory via `agentcache-python`.
4
+ You must act as your own memory manager by calling the cache MCP tools at critical boundaries.
5
 
6
  ## Rules & Workflow
7
 
8
  1. **Initial Search (Prefetch Context)**:
9
+ At the start of every session or new task, immediately call `cache_smart_search` with terms related to the current objective. This retrieves past architecture patterns, preferences, bug fixes, and past work.
10
+ - Example: `cache_smart_search(query="jwt token rotation logic")`
11
 
12
+ 2. **Log Observations**:
13
+ As you work, call `agent_observe` to record what you are doing. Include `folderPath` (the working directory), `agentId` (your agent name), and `text` (what happened).
14
+ - Example: `agent_observe(folderPath="/your/project/path", agentId="windsurf", text="Refactored auth middleware to use JWT rotation")`
15
+
16
+ 3. **Save Long-Term Memories**:
17
  When you successfully debug a complex error, discover an undocumented requirement, or establish a convention, persist it:
18
+ - Call `cache_save` to save long-term structural facts. Always extract 2-5 specific lowercased tags (e.g. `auth-flow`, `refresh-token`) as concepts.
 
19
 
20
+ 4. **Checklist Before Ending**:
21
  Before stating a task is complete:
22
+ - Reflect on whether any insights should be saved via `cache_save`.
23
+ - Review the folder activity feed with `cache_timeline` to confirm all significant work was captured.
AGENTS.md CHANGED
@@ -1,23 +1,53 @@
1
- # agentmemory-python — Agent Instructions
2
 
3
  ## What this project is
4
 
5
- A Python REST + WebSocket + MCP memory server backed by SQLite. No Node.js, no iii-engine, no Dolt. Agents use it to store observations, memories, lessons, and slots, and to retrieve context at session start.
6
 
7
  ## Project layout
8
 
9
  ```
10
  src/
11
- ├── app.py Flask serverall endpoints, WebSocket broadcaster
12
- ├── db.py SQLite StateKV WAL mode, audit_log table
13
- ├── functions.py Core business logic (observe, remember, search, context)
14
- ├── search.py BM25 index + Gemini vector index + HybridSearch (RRF)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  └── viewer/
16
- └── index.html Single-file HTML dashboard
 
17
  sync.py HuggingFace dataset backup/restore
18
  Dockerfile HF Space container definition
19
  start.sh Boot: restore DB → start server → start sync loop
20
  requirements.txt flask, flask-sock, requests, websockets, python-dateutil, huggingface_hub
 
 
21
  ```
22
 
23
  ## Running
@@ -29,130 +59,130 @@ python src/app.py
29
  # Viewer at http://localhost:3111/viewer
30
  ```
31
 
32
- No build step. No external database. SQLite file lives at `~/.agentmemory/agentmemory.db`.
33
 
34
  ## Architecture
35
 
 
 
 
 
36
  ### Storage — `src/db.py`
37
 
38
- `StateKV` wraps a single SQLite file with two tables:
 
 
 
39
 
40
- - `kv_store(scope TEXT, key TEXT, value TEXT, PRIMARY KEY(scope, key))` all data as JSON, namespaced by scope
41
- - `audit_log(id, ts, agent_id, message)` — write audit trail (replaces Dolt versioning)
42
 
43
- Key scopes (defined in `functions.py` `KV` class):
44
 
45
  | Scope | Content |
46
  |-------|---------|
47
- | `mem:sessions` | Session objects |
48
- | `mem:obs:{session_id}` | Observations for a session |
49
- | `mem:memories` | Long-term memories |
50
- | `mem:lessons` | Lessons with confidence scores |
51
- | `mem:slots` | Pinned memory slots |
 
 
52
  | `mem:relations` | Knowledge graph edges |
53
- | `mem:actions` | Work items / actions |
 
54
 
55
  ### Business logic — `src/functions.py`
56
 
57
- Global state:
58
- - `_bm25_index` / `_vector_index` — in-memory search indexes, rebuilt from DB on startup if empty
59
- - `_hybrid_search` — combines BM25 + vector; only initialized when Gemini key is set
60
- - `_stream_broadcaster` — WebSocket broadcast callback injected by `app.py`
61
 
62
  **Observation pipeline:**
63
  ```
64
- raw payload → strip_private_data() → build_synthetic_compression()
65
- stored in kv_storeBM25-indexedvector-indexed (if key set)
66
- audit_log entryWebSocket broadcast
 
67
  ```
68
 
69
  **Memory versioning:** `remember()` checks Jaccard similarity against existing memories. If > 0.7 match found, new memory supersedes old (`isLatest=False` on old, `parentId` set on new).
70
 
71
- **Context compilation** (`context()`): assembles pinned slots project profile lessons (scored by confidence × project match) past session summaries, capped at `TOKEN_BUDGET` tokens (estimated at `len/3`).
72
 
73
- **Lessons:** fingerprinted by SHA-256 of content. Duplicate saves strengthen confidence (`+0.1 × (1 - conf)`). Weekly decay reduces confidence by `decayRate × weeks`; soft-deleted at ≤ 0.1 confidence with 0 reinforcements.
74
 
75
  ### Search — `src/search.py`
76
 
77
- - `SearchIndex`: BM25 with custom Porter stemmer. Persisted to `kv_store` in sharded 2MB chunks via `IndexPersistence`.
78
- - `VectorIndex`: cosine similarity over Gemini 768-dim embeddings stored as base64-encoded float32 arrays.
79
- - `HybridSearch`: fuses BM25 + vector scores with RRF (k=60, reciprocal rank fusion).
80
 
81
- ### Server `src/app.py`
82
 
83
- Boot order:
84
- 1. Initialize `StateKV` (SQLite)
85
- 2. Initialize embedding provider (Gemini if key set)
86
- 3. Initialize `IndexPersistence`
87
- 4. Rebuild BM25/vector index if empty (background thread)
88
- 5. Start Flask on `III_REST_PORT` (default 3111)
89
 
90
- Auth: all endpoints check `AGENTMEMORY_SECRET` via timing-safe `hmac.compare_digest` Bearer token comparison if the env var is set. `/livez` is always open.
91
 
92
- WebSocket at `/stream/mem-live/viewer` broadcasts raw + compressed observations to connected viewers.
 
 
 
 
 
 
 
 
 
 
 
93
 
94
  ## MCP Tools
95
 
96
- The server exposes 31 MCP tools via `GET /agentmemory/mcp/tools` (schema) and `POST /agentmemory/mcp/tools` (execution).
97
 
98
  | Tool | Description | Status |
99
  |------|-------------|--------|
100
- | `memory_recall` | Search past session observations | Working |
101
- | `memory_save` | Save long-term memory (concepts/files as string or array) | Working |
102
- | `memory_sessions` | List recent sessions | Working |
103
- | `memory_sessions_list` | Retrieve all memory sessions | Working |
104
- | `memory_smart_search` | Hybrid semantic+keyword search | Working |
105
- | `memory_timeline` | Chronological observations | Working |
106
- | `memory_observations` | Get observations for session | Working |
107
- | `memory_profile` | User/project profile | Working |
108
- | `memory_lessons` | List saved lessons | Working |
109
- | `memory_lesson_save` | Save lesson from session | Working |
110
- | `memory_lesson_recall` | Search lessons by query | Working |
111
- | `memory_lesson_search` | Search lessons (keywords) | Working |
112
- | `memory_consolidate` | Summarize sessions, extract memory | Working |
113
- | `memory_reflect` | Reflect on session, update context | Working |
114
- | `memory_diagnose` | Health check subsystems | Working |
115
- | `memory_forget` | Delete memory/session/observations | Working |
116
- | `memory_export` | Export all data as JSON | Working |
117
- | `agent_observe` | Log agent execution observation | Working |
118
- | `agent_remember` | Save agent memory to long-term | Working |
119
- | `memory_antigravity_sync` | Sync Antigravity transcripts | Working |
120
- | `memory_antigravity_sync_all` | Master sync: transcript + crystallize + reflect | Working |
121
- | `memory_slot_list` | List all pinned memory slots | Working |
122
- | `memory_slot_get` | Retrieve a specific pinned slot | Working |
123
- | `memory_slot_create` | Create/overwrite pinned slot | Working |
124
- | `memory_slot_append` | Append text content to slot | Working |
125
- | `memory_slot_replace` | Replace slot content | Working |
126
- | `memory_slot_delete` | Delete pinned memory slot | Working |
127
- | `memory_action_create` | Create a new work item / action | Working |
128
- | `memory_action_update` | Update fields of existing action | Working |
129
- | `memory_frontier` | Get active/pending actions | Working |
130
- | `memory_crystallize` | Summarize session observations | Working |
131
-
132
- **MCP stdio wrapper:** `src/mcp_stdio.py` reads `AGENTMEMORY_URL` and `AGENTMEMORY_SECRET` from environment variables dynamically.
133
 
134
  ## Consistency rules
135
 
136
  **When adding a REST endpoint:**
137
- 1. Add the route in `src/app.py`
138
- 2. Update `API Reference` section in `README.md`
139
- 3. Add the MCP tool in `src/app.py` MCP dispatch if it should be agent-callable
140
 
141
  **When adding an MCP tool:**
142
- 1. Add the schema to the `GET /mcp/tools` response in `src/app.py`
143
- 2. Add the handler case to the `POST /mcp/tools` dispatch in `src/app.py`
144
  3. Update the tool table in `README.md`
145
  4. Update `AGENTS.md` tool list
146
 
147
  **When changing data scopes:**
148
- 1. Update the `KV` class in `src/functions.py`
149
- 2. Update the scope table in this file
 
150
 
151
  ## Code patterns
152
 
153
  ### Adding a new KV scope
154
 
155
  ```python
 
156
  class KV:
157
  your_scope = "mem:your-scope"
158
 
@@ -164,21 +194,21 @@ class KV:
164
  ### Adding a REST endpoint
165
 
166
  ```python
167
- @app.route('/agentmemory/your-path', methods=['POST'])
 
168
  def your_endpoint():
169
- if AGENTMEMORY_SECRET:
170
- auth = request.headers.get('Authorization', '')
171
- if not hmac.compare_digest(auth, f'Bearer {AGENTMEMORY_SECRET}'):
172
- return jsonify({'error': 'Unauthorized'}), 401
173
- body = request.get_json(silent=True) or {}
174
  # validate fields explicitly — never pass raw body to functions
175
- result = your_function(kv, body.get('field'))
176
  return jsonify(result), 200
177
  ```
178
 
179
  ### Adding an MCP tool schema
180
 
181
- In the `GET /mcp/tools` handler, add to the tools list:
182
  ```python
183
  {
184
  "name": "memory_your_tool",
@@ -193,11 +223,11 @@ In the `GET /mcp/tools` handler, add to the tools list:
193
  }
194
  ```
195
 
196
- In the `POST /mcp/tools` handler, add a case:
197
  ```python
198
  elif tool_name == 'memory_your_tool':
199
  query = args.get('query', '')
200
- result = your_function(kv, query)
201
  return jsonify({'content': [{'type': 'text', 'text': json.dumps(result)}]})
202
  ```
203
 
@@ -206,26 +236,44 @@ elif tool_name == 'memory_your_tool':
206
  | Variable | Default | Purpose |
207
  |----------|---------|---------|
208
  | `III_REST_PORT` / `PORT` | `3111` | Server port |
209
- | `GEMINI_API_KEY` / `GOOGLE_API_KEY` | — | Enables vector search + Gemini LLM |
210
- | `AGENTMEMORY_SECRET` | — | Bearer token auth |
 
 
211
  | `AGENT_ID` | — | Default agent ID |
212
- | `AGENTMEMORY_AGENT_SCOPE=isolated` | — | Filter data to current agent |
213
- | `MAX_OBS_PER_SESSION` | `500` | Observations hard cap |
 
214
  | `TOKEN_BUDGET` | `2000` | Context compilation cap |
215
  | `GRAPH_EXTRACTION_ENABLED` | `false` | Graph extraction (needs LLM) |
216
  | `CONSOLIDATION_ENABLED` | `false` | Consolidation (needs LLM) |
217
- | `AGENTMEMORY_AUTO_COMPRESS` | `false` | LLM compression |
 
 
 
218
  | `HF_TOKEN` | — | HuggingFace sync |
219
- | `AGENTMEMORY_DATASET_REPO` | — | HF dataset repo for backup |
220
 
221
- ## HuggingFace Space deployment
222
 
223
- Data flow: `agentmemory.db` (SQLite) ↔ `sync.py` ↔ HF dataset repo.
 
 
 
 
 
 
 
 
 
 
224
 
225
- `sync.py` uses mtime fingerprinting (`_quick_hash`) to detect changes before uploading. Backup only runs when the DB actually changed. Restore uses `hf_hub_download` for targeted file fetches rather than full `snapshot_download`.
 
 
226
 
227
  `start.sh` sequence:
228
- 1. Restore `agentmemory.db` from dataset repo
229
  2. Start `python src/app.py` in background
230
  3. Run `sync.py` in a loop (backup every ~60s if changed)
231
 
@@ -233,10 +281,9 @@ Data flow: `agentmemory.db` (SQLite) ↔ `sync.py` ↔ HF dataset repo.
233
 
234
  Single-file HTML dashboard, served directly by Flask at `/viewer`. No build step, no bundler.
235
 
236
- Tabs: Dashboard, Sessions, Memories, Graph, Timeline, Lessons, Slots, Replay.
237
-
238
- **Graph tab** (`loadGraph()`): fetches sessions + memories, groups by `project` path into folder nodes. Edges connect folders that share concepts or parent path segments. Each folder node gets a unique color via `folderColor(id)` — a hash-to-hex function that converts the folder path string into a distinct HSL color. The simulation uses force-directed physics with per-node-count repulsion tuning.
239
-
240
- ## No tests yet
241
 
242
- No test runner is configured. When adding tests, use `pytest` it's the standard Python choice and requires no extra config for basic test discovery (`test_*.py` files).
 
 
 
 
1
+ # agentcache-python — Agent Instructions
2
 
3
  ## What this project is
4
 
5
+ A Python REST + WebSocket + MCP memory server backed by SQLite. No Node.js, no iii-engine, no Dolt. Agents use it to store observations scoped to `(folder_path, agent_id)` pairs and global long-term memories. The viewer, MCP tools, and REST API are built around the **folder-based memory model** — sessions, lessons, slots, and actions are removed.
6
 
7
  ## Project layout
8
 
9
  ```
10
  src/
11
+ ├── app.py Thin Flask factorycreate_app(), WebSocket, CORS hook
12
+ ├── cli.py CLI entrypoint (agentcache serve/migrate/export)
13
+ ├── connect.py Client connection helper
14
+ ├── db.py StateKV SQLite WAL, per-thread connections, stats()
15
+ ├── functions.py All canonical business logic (large; memory/ shims delegate here)
16
+ ├── search.py BM25 + VectorIndex + HybridSearch + 3 embedding providers
17
+ ├── viewer_helpers.py Viewer HTML injection helper
18
+ ├── workers.py Background threads — index rebuild, auto-forget, SIGTERM handler
19
+
20
+ ├── routes/ Flask blueprints
21
+ │ ├── observations.py /observe, /agent/observe, /folders, /folder/observations
22
+ │ ├── memories.py /remember, /agent/remember, /memories, /forget
23
+ │ ├── search.py /search, /timeline
24
+ │ ├── graph.py /graph, /graph/stats, /graph/query, /graph/build
25
+ │ ├── health.py /livez, /health, /audit, /config/flags
26
+ │ ├── mcp.py /mcp/tools GET+POST (12 active tools)
27
+ │ └── migration.py /migrate
28
+
29
+ ├── memory/ Thin shim package — delegates to functions.py
30
+ │ ├── observe.py folder_observe, observe, build_synthetic_compression, strip_private_data
31
+ │ ├── remember.py remember, forget, jaccard_similarity
32
+ │ ├── context.py context, export_data, rebuild_index
33
+ │ ├── graph.py folder_graph_build
34
+ │ ├── timeline.py folder_timeline, folder_search
35
+ │ └── health.py health_check, auto_forget
36
+
37
+ ├── storage/ KV scope registry + path/ID utilities
38
+ │ ├── scopes.py KV class (mirrored from functions.py)
39
+ │ ├── paths.py normalize_folder_path, validate_agent_id, generate_id, fingerprint_id
40
+ │ └── images.py save_image_to_disk, delete_image, touch_image
41
+
42
  └── viewer/
43
+ └── index.html Single-file HTML dashboard (4 tabs: Folders / Memories / Graph / Timeline)
44
+
45
  sync.py HuggingFace dataset backup/restore
46
  Dockerfile HF Space container definition
47
  start.sh Boot: restore DB → start server → start sync loop
48
  requirements.txt flask, flask-sock, requests, websockets, python-dateutil, huggingface_hub
49
+ pyproject.toml pip-installable package (agentcache==0.9.8, Python ≥3.10)
50
+ tests/ pytest suite — unit, integration, and Hypothesis property tests
51
  ```
52
 
53
  ## Running
 
59
  # Viewer at http://localhost:3111/viewer
60
  ```
61
 
62
+ No build step. No external database. SQLite file lives at `~/.agentcache/agentcache.db`.
63
 
64
  ## Architecture
65
 
66
+ ### Data Model — Folder-Based Memory
67
+
68
+ The primary unit of storage is `(folder_path, agent_id)`. Each agent accumulates observations scoped to the folder it is working in. Global long-term memories remain unchanged.
69
+
70
  ### Storage — `src/db.py`
71
 
72
+ `StateKV` wraps a single SQLite file with:
73
+ - `kv_store(scope TEXT, key TEXT, value TEXT, PRIMARY KEY(scope, key))` — all data as JSON
74
+ - `audit_log(id, ts, agent_id, message)` — write audit trail
75
+ - `sync_state_metadata(key, value)` — HuggingFace sync watermark
76
 
77
+ Per-thread persistent connections via `threading.local()`. WAL checkpoint registered via `atexit` and on `SIGTERM`/`SIGINT`.
 
78
 
79
+ Key scopes (defined in `functions.py` `KV` class and mirrored in `src/storage/scopes.py`):
80
 
81
  | Scope | Content |
82
  |-------|---------|
83
+ | `mem:folders` | Index of all `(folder_path, agent_id)` pairs — key = `"{path}:{agent}"` |
84
+ | `mem:folder:{path}:{agent}` | Observations for a pair — key = obs_id |
85
+ | `mem:foldermeta:{path}:{agent}` | Metadata for a pair (obsCount, lastUpdated, summary) |
86
+ | `mem:obs_lookup` | O(1) reverse-lookup: obs_id `{folderPath, agentId}` |
87
+ | `mem:memories` | Global long-term memories |
88
+ | `mem:index:bm25:*` | Sharded BM25 index (2MB chunks) |
89
+ | `mem:audit` | Audit log entries |
90
  | `mem:relations` | Knowledge graph edges |
91
+ | `mem:sessions` | Legacy session store (read-only, migration only) |
92
+ | `mem:obs:{session_id}` | Legacy per-session observations (read-only, migration only) |
93
 
94
  ### Business logic — `src/functions.py`
95
 
96
+ All canonical implementations live here. `src/memory/*` are thin shims that re-export from this module (for future decoupling).
 
 
 
97
 
98
  **Observation pipeline:**
99
  ```
100
+ raw payload → normalize_folder_path() → validate_agent_id() → strip_private_data()
101
+ build obs dictkv.set(folder_obs scope) upsert folder_meta + folders index
102
+ kv.set(obs_lookup) BM25-indexed vector-indexed (if key set)
103
+ → schedule_save() (debounced 5s) → audit log → WebSocket broadcast
104
  ```
105
 
106
  **Memory versioning:** `remember()` checks Jaccard similarity against existing memories. If > 0.7 match found, new memory supersedes old (`isLatest=False` on old, `parentId` set on new).
107
 
108
+ **Search:** `folder_search()` uses `HybridSearch` (BM25 + vector, RRF k=60). Falls back to BM25-only when no embedding provider is configured. Results include both folder observations and global memories.
109
 
110
+ **`health_check()`** returns: `folderCount`, `agentCount`, `pairCount`, `observationCount`, `memoryCount`, `bm25IndexSize`, `vectorIndexSize`, `dbPath`, plus `db.stats()` fields.
111
 
112
  ### Search — `src/search.py`
113
 
114
+ - `SearchIndex`: BM25 with Porter stemmer and synonym expansion. Dirty-flag (`_dirty`) prevents unnecessary saves. Persisted in sharded 2MB KV chunks.
115
+ - `VectorIndex`: cosine similarity over embeddings stored as base64-encoded float32 arrays. Also has `_dirty` flag.
116
+ - `HybridSearch`: fuses BM25 + vector scores with RRF (k=60).
117
 
118
+ **Embedding providers** (auto-selected by priority in `create_app()`):
119
 
120
+ | Priority | Provider | Env var | Dimensions |
121
+ |----------|----------|---------|------------|
122
+ | 1 | `GeminiEmbeddingProvider` | `GEMINI_API_KEY` / `GOOGLE_API_KEY` | 768 |
123
+ | 2 | `OpenAIEmbeddingProvider` | `OPENAI_API_KEY` | 1536 |
124
+ | 3 | `SentenceTransformerProvider` | `AGENTCACHE_LOCAL_EMBEDDING_MODEL` | variable |
125
+ | 4 | BM25-only | | — |
126
 
127
+ ### Server `src/app.py`
128
 
129
+ Boot order:
130
+ 1. Load `~/.agentcache/.env` if present
131
+ 2. Initialize `StateKV` (SQLite)
132
+ 3. Auto-select embedding provider (Gemini → OpenAI → SentenceTransformer → BM25-only)
133
+ 4. Initialize `IndexPersistence` (load or rebuild)
134
+ 5. Backfill `obs_lookup` index if missing
135
+ 6. Create Flask app, register blueprints
136
+ 7. Set up WebSocket `/stream/mem-live/viewer`
137
+ 8. Register CORS `after_request` hook
138
+ 9. Start background workers (index rebuild if empty/stale, auto-forget loop)
139
+
140
+ Auth: all endpoints check `AGENTCACHE_SECRET` via `hmac.compare_digest`. `/livez` is always open.
141
 
142
  ## MCP Tools
143
 
144
+ The server exposes **12 MCP tools** via `GET /agentcache/mcp/tools` and `POST /agentcache/mcp/tools`.
145
 
146
  | Tool | Description | Status |
147
  |------|-------------|--------|
148
+ | `agent_observe` | Log observation to a `(folderPath, agentId)` pair | Working |
149
+ | `agent_remember` | Save to global long-term memory | Working |
150
+ | `memory_recall` | Search folder obs + global memories (BM25+vector) | Working |
151
+ | `memory_smart_search` | Hybrid semantic+keyword search (alias for recall) | Working |
152
+ | `memory_save` | Explicitly save insight to long-term memory | Working |
153
+ | `memory_export` | Export all data as JSON (v2 format) | Working |
154
+ | `memory_forget` | Delete memory or folder pair observations | Working |
155
+ | `memory_diagnose` | Health check (folder/agent/obs/memory counts) | Working |
156
+ | `memory_folders` | List all `(folder, agent)` pairs | Working |
157
+ | `memory_folder_observations` | Get observations for a specific pair | Working |
158
+ | `memory_timeline` | Folder activity feed (sorted by time, filterable) | Working |
159
+
160
+ **MCP stdio wrapper:** `src/mcp_stdio.py` reads `AGENTCACHE_URL` and `AGENTCACHE_SECRET` from environment variables dynamically.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
 
162
  ## Consistency rules
163
 
164
  **When adding a REST endpoint:**
165
+ 1. Add the route in the appropriate `src/routes/*.py` blueprint
166
+ 2. Update the `API Reference` section in `README.md`
167
+ 3. Add the MCP tool in `src/routes/mcp.py` if it should be agent-callable
168
 
169
  **When adding an MCP tool:**
170
+ 1. Add the schema to the `GET /mcp/tools` response in `src/routes/mcp.py`
171
+ 2. Add the handler case to the `POST /mcp/tools` dispatch in `src/routes/mcp.py`
172
  3. Update the tool table in `README.md`
173
  4. Update `AGENTS.md` tool list
174
 
175
  **When changing data scopes:**
176
+ 1. Update the `KV` class in `src/functions.py` (canonical)
177
+ 2. Mirror the change in `src/storage/scopes.py`
178
+ 3. Update the scope table in this file
179
 
180
  ## Code patterns
181
 
182
  ### Adding a new KV scope
183
 
184
  ```python
185
+ # In src/functions.py KV class (canonical):
186
  class KV:
187
  your_scope = "mem:your-scope"
188
 
 
194
  ### Adding a REST endpoint
195
 
196
  ```python
197
+ # In the appropriate src/routes/*.py blueprint:
198
+ @your_bp.route('/agentcache/your-path', methods=['POST'])
199
  def your_endpoint():
200
+ auth_err = _check_auth()
201
+ if auth_err:
202
+ return auth_err
203
+ body = request.get_json(force=True) or {}
 
204
  # validate fields explicitly — never pass raw body to functions
205
+ result = functions.your_function(_get_kv(), body.get('field'))
206
  return jsonify(result), 200
207
  ```
208
 
209
  ### Adding an MCP tool schema
210
 
211
+ In `src/routes/mcp.py`, `GET /agentcache/mcp/tools` handler:
212
  ```python
213
  {
214
  "name": "memory_your_tool",
 
223
  }
224
  ```
225
 
226
+ In `src/routes/mcp.py`, `POST /agentcache/mcp/tools` handler:
227
  ```python
228
  elif tool_name == 'memory_your_tool':
229
  query = args.get('query', '')
230
+ result = functions.your_function(kv, query)
231
  return jsonify({'content': [{'type': 'text', 'text': json.dumps(result)}]})
232
  ```
233
 
 
236
  | Variable | Default | Purpose |
237
  |----------|---------|---------|
238
  | `III_REST_PORT` / `PORT` | `3111` | Server port |
239
+ | `GEMINI_API_KEY` / `GOOGLE_API_KEY` | — | Enables Gemini vector search (priority 1) |
240
+ | `OPENAI_API_KEY` | — | Enables OpenAI vector search (priority 2) |
241
+ | `AGENTCACHE_LOCAL_EMBEDDING_MODEL` | — | SentenceTransformer model name (priority 3) |
242
+ | `AGENTCACHE_SECRET` | — | Bearer token auth |
243
  | `AGENT_ID` | — | Default agent ID |
244
+ | `AGENTCACHE_AGENT_SCOPE=isolated` | — | Filter data to current agent |
245
+ | `AGENTCACHE_CWD` | | Fallback folder path for legacy clients |
246
+ | `MAX_OBS_PER_FOLDER` | `2000` | Observations hard cap per (folder, agent) pair |
247
  | `TOKEN_BUDGET` | `2000` | Context compilation cap |
248
  | `GRAPH_EXTRACTION_ENABLED` | `false` | Graph extraction (needs LLM) |
249
  | `CONSOLIDATION_ENABLED` | `false` | Consolidation (needs LLM) |
250
+ | `AGENTCACHE_AUTO_COMPRESS` | `false` | LLM compression |
251
+ | `AUTO_FORGET_ENABLED` | — | Auto-forget sweep (set to "false" to disable) |
252
+ | `AGENTCACHE_CORS_ORIGINS` | see app.py | Comma-separated allowed origins |
253
+ | `AGENTCACHE_IMAGE_STORE_MAX_BYTES` | 500MB | Image store byte limit |
254
  | `HF_TOKEN` | — | HuggingFace sync |
255
+ | `AGENTCACHE_DATASET_REPO` | — | HF dataset repo for backup |
256
 
257
+ ## Testing
258
 
259
+ ```bash
260
+ pip install -e ".[dev]"
261
+ pytest tests/ -v
262
+ ```
263
+
264
+ Tests live in `tests/` — 17 test files covering unit tests, integration tests (Flask test client), and Hypothesis property tests.
265
+
266
+ Key test files:
267
+ - `tests/test_properties.py` — 8 correctness properties (pair isolation, obs count consistency, index coverage, privacy, timeline ordering, forget completeness, memory version uniqueness, path normalization idempotency)
268
+ - `tests/test_api.py` — Flask test client integration tests
269
+ - `tests/test_route_regressions.py` — regression suite after blueprint split
270
 
271
+ ## HuggingFace Space deployment
272
+
273
+ Data flow: `agentcache.db` (SQLite) ↔ `sync.py` ↔ HF dataset repo.
274
 
275
  `start.sh` sequence:
276
+ 1. Restore `agentcache.db` from dataset repo
277
  2. Start `python src/app.py` in background
278
  3. Run `sync.py` in a loop (backup every ~60s if changed)
279
 
 
281
 
282
  Single-file HTML dashboard, served directly by Flask at `/viewer`. No build step, no bundler.
283
 
284
+ Tabs: **Folders**, **Memories**, **Graph**, **Timeline**.
 
 
 
 
285
 
286
+ - **Folders tab**: lists all `(folder, agent)` pairs; click a row to drill into observations
287
+ - **Memories tab**: global long-term memories with search
288
+ - **Graph tab**: force-directed graph — nodes = folder paths, edges = same-parent / cross-ref / agent-shared
289
+ - **Timeline tab**: all observations sorted by timestamp desc, filterable by folder path and agent ID
ARCHITECTURE.md CHANGED
@@ -1,4 +1,4 @@
1
- # agentmemory-python — Architecture
2
 
3
  ## Overview
4
 
@@ -44,8 +44,8 @@ src/
44
  ├── viewer_helpers.py make_viewer_response() — reads viewer/index.html,
45
  │ injects nonce + version, sets CSP headers.
46
 
47
- ├── mcp_stdio.py stdio MCP bridge: reads AGENTMEMORY_URL and
48
- AGENTMEMORY_SECRET, proxies tool calls to the HTTP API.
49
 
50
  └── viewer/
51
  └── index.html Single-file HTML dashboard (no bundler).
@@ -55,7 +55,7 @@ src/
55
 
56
  ## KV Scope Layout
57
 
58
- All data lives in a single SQLite file (`~/.agentmemory/agentmemory.db`) in two tables:
59
 
60
  - `kv_store(scope TEXT, key TEXT, value TEXT, PRIMARY KEY(scope, key))` — JSON values
61
  - `audit_log(id, ts, agent_id, message)` — write audit trail
@@ -120,7 +120,7 @@ If found, old memory is marked `isLatest=False` and new memory sets `parentId`.
120
 
121
  ## Authentication
122
 
123
- All endpoints except `/livez` check `AGENTMEMORY_SECRET` via timing-safe
124
  `hmac.compare_digest` Bearer token comparison. No secret → no auth check.
125
 
126
  ---
@@ -138,7 +138,7 @@ Priority order (auto-selected at startup):
138
 
139
  1. `GeminiEmbeddingProvider` — if `GEMINI_API_KEY` or `GOOGLE_API_KEY` is set (768 dims)
140
  2. `OpenAIEmbeddingProvider` — if `OPENAI_API_KEY` is set (1536 dims)
141
- 3. `SentenceTransformerProvider` — if `AGENTMEMORY_LOCAL_EMBEDDING_MODEL` is set
142
  4. BM25-only fallback
143
 
144
  Without an embedding provider, `HybridSearch` falls back to pure BM25.
 
1
+ # agentcache-python — Architecture
2
 
3
  ## Overview
4
 
 
44
  ├── viewer_helpers.py make_viewer_response() — reads viewer/index.html,
45
  │ injects nonce + version, sets CSP headers.
46
 
47
+ ├── mcp_stdio.py stdio MCP bridge: reads AGENTCACHE_URL and
48
+ AGENTCACHE_SECRET, proxies tool calls to the HTTP API.
49
 
50
  └── viewer/
51
  └── index.html Single-file HTML dashboard (no bundler).
 
55
 
56
  ## KV Scope Layout
57
 
58
+ All data lives in a single SQLite file (`~/.agentcache/agentcache.db`) in two tables:
59
 
60
  - `kv_store(scope TEXT, key TEXT, value TEXT, PRIMARY KEY(scope, key))` — JSON values
61
  - `audit_log(id, ts, agent_id, message)` — write audit trail
 
120
 
121
  ## Authentication
122
 
123
+ All endpoints except `/livez` check `AGENTCACHE_SECRET` via timing-safe
124
  `hmac.compare_digest` Bearer token comparison. No secret → no auth check.
125
 
126
  ---
 
138
 
139
  1. `GeminiEmbeddingProvider` — if `GEMINI_API_KEY` or `GOOGLE_API_KEY` is set (768 dims)
140
  2. `OpenAIEmbeddingProvider` — if `OPENAI_API_KEY` is set (1536 dims)
141
+ 3. `SentenceTransformerProvider` — if `AGENTCACHE_LOCAL_EMBEDDING_MODEL` is set
142
  4. BM25-only fallback
143
 
144
  Without an embedding provider, `HybridSearch` falls back to pure BM25.
CLAUDE.md CHANGED
@@ -4,100 +4,106 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
4
 
5
  ## What This Is
6
 
7
- Python reimplementation of the agentmemory persistent memory server. Exposes a REST API + WebSocket stream + MCP tools endpoint that AI coding agents use to store and retrieve session observations, long-term memories, lessons, and pinned memory slots. Backed by a Dolt SQL Server (MySQL-compatible).
8
 
9
  ## Running
10
 
11
- **Prerequisite**: Dolt SQL Server must be running on `127.0.0.1:3306` with a database named `agentmemory`. Config is read from `~/.agentmemory/.env` at startup.
12
-
13
  ```bash
14
- # Start the memory API server and built-in viewer (port 3111)
15
  python src/app.py
 
 
16
  ```
17
 
18
- The built-in HTML dashboard is accessible at:
19
- - `http://localhost:3111/viewer` or `http://localhost:3111/`
20
 
21
- No build step. No test runner is configured yet.
22
 
23
- ## Key Environment Variables
 
 
 
24
 
25
- Set in `~/.agentmemory/.env` or as system env vars:
26
 
27
  | Variable | Default | Purpose |
28
  |---|---|---|
29
  | `III_REST_PORT` / `PORT` | `3111` | API server port |
30
- | `DOLT_HOST/PORT/USER/PASSWORD/DATABASE` | `127.0.0.1/3306/root//"agentmemory"` | Dolt connection |
31
- | `GEMINI_API_KEY` / `GOOGLE_API_KEY` | — | Enables 768-dim vector search; without it, BM25-only |
32
- | `AGENTMEMORY_SECRET` | — | Enables Bearer token auth on all endpoints |
33
- | `AGENT_ID` | — | Default agent ID for scope isolation |
34
- | `AGENTMEMORY_AGENT_SCOPE=isolated` | — | Filters sessions/obs to current `AGENT_ID` |
35
- | `DOLT_AUTO_COMMIT=false` | auto-commit | Disable Dolt versioning commits per write |
36
- | `MAX_OBS_PER_SESSION` | `500` | Hard cap on observations per session |
37
- | `TOKEN_BUDGET` | `2000` | Max tokens in compiled context response |
38
- | `GRAPH_EXTRACTION_ENABLED=true` | `false` | Knowledge graph extraction (requires LLM) — **disabled by default** |
39
- | `CONSOLIDATION_ENABLED=true` | `false` | Memory consolidation (requires LLM) — **disabled by default** |
40
- | `AGENTMEMORY_AUTO_COMPRESS=true` | `false` | LLM-powered observation compression |
 
 
41
 
42
  ## Architecture
43
 
44
  ### `src/db.py` — Storage Layer
45
- `StateKV` wraps a single Dolt table `kv_store(scope VARCHAR, key VARCHAR, value LONGTEXT)`. All data is JSON-serialized. Scopes are namespaced strings (e.g. `mem:sessions`, `mem:obs:{session_id}`). Dolt versioning is triggered via `CALL dolt_add('-A')` + `CALL dolt_commit(...)` stored procedures — this is what makes the store git-versioned.
46
 
47
- ### `src/functions.py` Business Logic
48
- All core operations live here. Important globals:
49
- - `_bm25_index` / `_vector_index`in-memory search indexes (rebuilt from DB on startup if empty)
50
- - `_hybrid_search` — combines BM25 + vector search; only initialized when embedding provider is set
51
- - `_stream_broadcaster` — WebSocket broadcast callback injected by `app.py`
52
 
53
- Key scopes are defined in the `KV` class. Dynamic scopes: `KV.observations(session_id)` `mem:obs:{session_id}`.
54
 
55
- **Observation pipeline**: raw payload `strip_private_data()` `build_synthetic_compression()` stored + BM25-indexed + vector-indexed + Dolt-committed + WebSocket-broadcast.
56
 
57
- **Memory versioning**: `remember()` checks Jaccard similarity against existing memories; if > 0.7 match found, the new memory supersedes the old one (`isLatest=False` on old, `parentId` set on new).
58
 
59
- **Context compilation** (`context()`): assembles pinned slots → project profile → lessons (scored by confidence × project match) → past session summaries, capped at `TOKEN_BUDGET` tokens (estimated at `len/3`).
 
 
 
 
 
 
 
 
60
 
61
- **Lessons**: fingerprinted by SHA-256 of content. Duplicate saves strengthen confidence (`+0.1 × (1 - conf)`). Weekly decay sweep reduces confidence by `decayRate × weeks`; soft-deleted at ≤ 0.1 confidence with 0 reinforcements.
62
 
63
  ### `src/search.py` — Search Indexes
64
- - `SearchIndex`: BM25 with custom Porter stemmer. Persisted to Dolt in sharded 2MB chunks via `IndexPersistence`.
65
- - `VectorIndex`: cosine similarity over Gemini 768-dim embeddings stored as base64-encoded float32 arrays.
66
- - `HybridSearch`: fuses BM25 + vector scores with RRF (reciprocal rank fusion).
67
-
68
- ### `src/app.py` Flask API
69
- Initializes DB → embedding provider → index persistence → rebuilds index if empty (background thread). All endpoints check `AGENTMEMORY_SECRET` via timing-safe Bearer token comparison. WebSocket at `/stream/mem-live/viewer` broadcasts raw + compressed observations to connected viewers.
70
-
71
- MCP tools are served at `GET /agentmemory/mcp/tools` (schema list) and `POST /agentmemory/mcp/tools` (tool call dispatch).
72
-
73
- ### `src/viewer/index.html` — Built-in HTML Dashboard
74
- Interactive web dashboard served directly by the Flask server. Provides real-time view of active sessions, timelines, memories with search, slots editor, and DB migration panel. Connects to the Flask backend via REST and live WebSockets. Imports legacy TypeScript `.bin` files via `src/import_data.py`.
75
-
76
- ## API Surface
77
-
78
- Base path: `/agentmemory/`
79
-
80
- - `GET /livez` — health/liveness (no auth)
81
- - `POST /observe` ingest a hook event observation
82
- - `POST /agent/observe` simplified observe for direct agent use
83
- - `POST /remember` / `POST /agent/remember` save long-term memory
84
- - `POST /forget` delete memory/session/observations
85
- - `POST /context` — compile context for a session+project
86
- - `POST /search` — hybrid BM25+vector search
87
- - `POST/GET /lessons` — lessons CRUD + `/lessons/search`, `/lessons/strengthen`
88
- - `GET/POST /slots`, `GET/POST/DELETE /slot` memory slots CRUD
89
- - `POST /slot/reflect` — auto-populate slots from session observations
90
- - `POST/GET /session/start|end|commit` session lifecycle
91
- - `GET /sessions`, `GET /observations` — list data
92
- - `GET/POST /relations` — knowledge graph edges
93
- - `POST /evolve` — create new memory version
94
- - `POST /timeline` chronological observation window
95
- - `GET /profile` — project profile (top concepts/files); no `?project` → returns list of all known projects
96
- - `GET /actions` — list actions (`?limit`, `?status`)
97
- - `POST /actions` — create action
98
- - `PATCH /actions/<id>` update action status/fields
99
- - `GET /frontier` — pending+active actions sorted by priority
100
- - `GET /insights` — list insights (`?limit`)
101
- - `GET /replay/sessions` — sessions list for replay tab
102
- - `GET /replay/load?sessionId=<id>` — full session + observations for replay
103
- - `GET/POST /mcp/tools` — MCP protocol adapter
 
4
 
5
  ## What This Is
6
 
7
+ A Python REST + WebSocket + MCP memory server backed by SQLite. No Node.js, no Dolt. Agents use it to store observations scoped to `(folder_path, agent_id)` pairs and global long-term memories. The architecture is **folder-based** sessions, lessons, slots, and actions are removed.
8
 
9
  ## Running
10
 
 
 
11
  ```bash
12
+ pip install -r requirements.txt
13
  python src/app.py
14
+ # Server on http://localhost:3111
15
+ # Viewer at http://localhost:3111/viewer
16
  ```
17
 
18
+ No build step. SQLite file lives at `~/.agentcache/agentcache.db`. Config optionally loaded from `~/.agentcache/.env`.
 
19
 
20
+ ## Running Tests
21
 
22
+ ```bash
23
+ pip install -e ".[dev]"
24
+ pytest tests/ -v
25
+ ```
26
 
27
+ ## Key Environment Variables
28
 
29
  | Variable | Default | Purpose |
30
  |---|---|---|
31
  | `III_REST_PORT` / `PORT` | `3111` | API server port |
32
+ | `GEMINI_API_KEY` / `GOOGLE_API_KEY` | | Gemini vector search (priority 1) |
33
+ | `OPENAI_API_KEY` | — | OpenAI vector search (priority 2) |
34
+ | `AGENTCACHE_LOCAL_EMBEDDING_MODEL` | — | SentenceTransformer model (priority 3) |
35
+ | `AGENTCACHE_SECRET` | — | Bearer token auth on all endpoints |
36
+ | `AGENT_ID` | — | Default agent ID |
37
+ | `AGENTCACHE_AGENT_SCOPE=isolated` | | Filter data to current `AGENT_ID` |
38
+ | `AGENTCACHE_CWD` | | Fallback folder path for legacy clients |
39
+ | `MAX_OBS_PER_FOLDER` | `2000` | Hard cap on observations per (folder, agent) pair |
40
+ | `TOKEN_BUDGET` | `2000` | Context compilation cap |
41
+ | `GRAPH_EXTRACTION_ENABLED` | `false` | Knowledge graph extraction (needs LLM) |
42
+ | `CONSOLIDATION_ENABLED` | `false` | Memory consolidation (needs LLM) |
43
+ | `AGENTCACHE_AUTO_COMPRESS` | `false` | LLM-powered observation compression |
44
+ | `AGENTCACHE_CORS_ORIGINS` | see app.py | Comma-separated allowed CORS origins |
45
 
46
  ## Architecture
47
 
48
  ### `src/db.py` — Storage Layer
 
49
 
50
+ `StateKV` wraps SQLite with two tables:
51
+ - `kv_store(scope, key, value)` all data as JSON, namespaced by scope
52
+ - `audit_log(id, ts, agent_id, message)` — write audit trail
 
 
53
 
54
+ Per-thread persistent connections via `threading.local()`. WAL checkpoint on shutdown.
55
 
56
+ Key scopes: `mem:folders` (index), `mem:folder:{path}:{agent}` (observations), `mem:foldermeta:{path}:{agent}` (metadata), `mem:obs_lookup` (O(1) reverse lookup), `mem:memories` (global), `mem:index:bm25:*` (search).
57
 
58
+ ### `src/functions.py` Business Logic
59
 
60
+ All core implementations. Key functions:
61
+ - `folder_observe(kv, payload)` — ingest a folder-scoped observation
62
+ - `folder_search(kv, query, limit, folder_path, agent_id)` — BM25+vector hybrid search
63
+ - `folder_timeline(kv, limit, folder_path, agent_id, before, after)` — activity feed
64
+ - `folder_graph_build(kv)` — build graph nodes + edges
65
+ - `remember(kv, data)` / `forget(kv, data)` — global memory management
66
+ - `health_check(kv)` — system stats
67
+ - `export_data(kv, data)` — v2 JSON export
68
+ - `migrate_sessions_to_folders(kv, dry_run)` — legacy migration
69
 
70
+ `src/memory/*` contains thin shim modules that re-export from here.
71
 
72
  ### `src/search.py` — Search Indexes
73
+
74
+ - `SearchIndex`: BM25 with Porter stemmer + synonym expansion. `_dirty` flag prevents unnecessary saves.
75
+ - `VectorIndex`: cosine similarity over embeddings as base64 float32. `_dirty` flag.
76
+ - `HybridSearch`: RRF fusion (k=60) of BM25 + vector.
77
+ - `GeminiEmbeddingProvider`, `OpenAIEmbeddingProvider`, `SentenceTransformerProvider`.
78
+
79
+ ### `src/app.py` — Flask Factory
80
+
81
+ `create_app()`: init DB → auto-select embedding provider → init IndexPersistence → backfill obs_lookup → register blueprints → setup WebSocket → register CORS → start background workers.
82
+
83
+ ### `src/routes/` Flask Blueprints
84
+
85
+ | Blueprint | Endpoints |
86
+ |-----------|-----------|
87
+ | `observations.py` | `/observe`, `/agent/observe`, `/folders`, `/folder/observations`, legacy session shims |
88
+ | `memories.py` | `/remember`, `/agent/remember`, `/memories`, `/forget` |
89
+ | `search.py` | `/search`, `/timeline` |
90
+ | `graph.py` | `/graph`, `/graph/stats`, `/graph/query`, `/graph/build` |
91
+ | `health.py` | `/livez`, `/health`, `/audit`, `/config/flags` |
92
+ | `mcp.py` | `/mcp/tools` GET+POST (12 tools) |
93
+ | `migration.py` | `/migrate` |
94
+
95
+ ### `src/workers.py` — Background Threads
96
+
97
+ - Index rebuild thread (if BM25 index is empty or out of sync on boot)
98
+ - Auto-forget sweep loop (hourly)
99
+ - SIGTERM/SIGINT handlers: flush `IndexPersistence`, WAL checkpoint, exit 0
100
+
101
+ ### `src/viewer/index.html` — Dashboard
102
+
103
+ Single-file HTML, served at `/viewer`. Tabs: **Folders**, **Memories**, **Graph**, **Timeline**. No build step.
104
+
105
+ ## MCP Tools (12 active)
106
+
107
+ `agent_observe`, `agent_remember`, `memory_recall`, `memory_smart_search`, `memory_save`, `memory_export`, `memory_forget`, `memory_diagnose`, `memory_folders`, `memory_folder_observations`, `memory_timeline`
108
+
109
+ Full schema at `GET /agentcache/mcp/tools`.
 
 
 
Caddyfile CHANGED
@@ -4,16 +4,16 @@
4
  handle /stream/* {
5
  reverse_proxy localhost:3112 {
6
  header_up Host localhost:3112
7
- header_up Authorization "Bearer {env.AGENTMEMORY_SECRET}"
8
  }
9
  }
10
 
11
  # Route REST API endpoints (iii-http on 3111)
12
  # Caddy injects the secret so the dashboard viewer works without exposing it to the browser
13
- handle /agentmemory/* {
14
  reverse_proxy localhost:3111 {
15
  header_up Host localhost:3111
16
- header_up Authorization "Bearer {env.AGENTMEMORY_SECRET}"
17
  }
18
  }
19
 
 
4
  handle /stream/* {
5
  reverse_proxy localhost:3112 {
6
  header_up Host localhost:3112
7
+ header_up Authorization "Bearer {env.AGENTCACHE_SECRET}"
8
  }
9
  }
10
 
11
  # Route REST API endpoints (iii-http on 3111)
12
  # Caddy injects the secret so the dashboard viewer works without exposing it to the browser
13
+ handle /agentcache/* /agentmemory/* {
14
  reverse_proxy localhost:3111 {
15
  header_up Host localhost:3111
16
+ header_up Authorization "Bearer {env.AGENTCACHE_SECRET}"
17
  }
18
  }
19
 
INSTALL_FOR_AGENTS.md CHANGED
@@ -1,10 +1,10 @@
1
- # Install agentmemory-python (agent runbook)
2
 
3
- You are a coding agent. Follow this runbook top to bottom to install agentmemory-python, wire it into yourself, and verify that persistent memory works. Run each command, check the result against the "Expect" note, then move on.
4
 
5
  ## What you are installing
6
 
7
- agentmemory-python is a local memory server for coding agents. It runs on your machine, exposes a REST API on port 3111 and an MCP endpoint, and stores all memories in a single SQLite file at `~/.agentmemory/agentmemory.db`. It is built with Python and Flask — no Node.js, no external database, no daemon to manage separately.
8
 
9
  Default mode works with no API key. It runs BM25 keyword search out of the box. Set `GEMINI_API_KEY` to also enable vector semantic search.
10
 
@@ -12,13 +12,13 @@ Default mode works with no API key. It runs BM25 keyword search out of the box.
12
 
13
  - Python 3.10 or newer. Check with `python --version` or `python3 --version`.
14
  - pip. Check with `pip --version`.
15
- - Port 3111 free. If taken, set `III_REST_PORT=<other>` in `~/.agentmemory/.env`.
16
 
17
  ## 1. Clone the repo
18
 
19
  ```bash
20
- git clone https://github.com/Yash030/agentmemory-python.git
21
- cd agentmemory-python
22
  ```
23
 
24
  Expect: the directory exists with `src/app.py` inside.
@@ -44,7 +44,7 @@ Or run in a separate terminal: `python src/app.py`
44
  Wait until it is reachable:
45
 
46
  ```bash
47
- curl -fsS http://localhost:3111/agentmemory/livez
48
  ```
49
 
50
  Expect: `{"status": "ok"}`. On first run the BM25 index initializes in the background; this takes under a second.
@@ -53,12 +53,12 @@ Expect: `{"status": "ok"}`. On first run the BM25 index initializes in the backg
53
 
54
  ```bash
55
  # Save a memory
56
- curl -X POST http://localhost:3111/agentmemory/remember \
57
  -H "Content-Type: application/json" \
58
- -d '{"content":"agentmemory-python install verification probe","concepts":["install-check"]}'
59
 
60
  # Recall it
61
- curl -X POST http://localhost:3111/agentmemory/search \
62
  -H "Content-Type: application/json" \
63
  -d '{"query":"install verification probe","limit":5}'
64
  ```
@@ -67,18 +67,18 @@ Expect: first call returns `{"id": "...", "success": true}`, second returns resu
67
 
68
  ## 5. Wire MCP into the calling agent
69
 
70
- Detect which agent is running this runbook, then add agentmemory-python to its MCP config.
71
 
72
  ### Standard MCP block (Cursor, Claude Desktop, Cline, Windsurf, Gemini CLI)
73
 
74
  Merge this into the agent's `mcpServers` object:
75
 
76
  ```json
77
- "agentmemory": {
78
  "command": "npx",
79
- "args": ["-y", "@agentmemory/mcp"],
80
  "env": {
81
- "AGENTMEMORY_URL": "http://localhost:3111"
82
  }
83
  }
84
  ```
@@ -88,11 +88,11 @@ Merge this into the agent's `mcpServers` object:
88
  Add to `~/.claude.json` under `mcpServers`:
89
 
90
  ```json
91
- "agentmemory": {
92
  "command": "npx",
93
- "args": ["-y", "@agentmemory/mcp"],
94
  "env": {
95
- "AGENTMEMORY_URL": "http://localhost:3111"
96
  }
97
  }
98
  ```
@@ -101,17 +101,17 @@ Then reload MCP: run `/mcp` in Claude Code.
101
 
102
  ### Any agent — verify tool count
103
 
104
- After wiring, the agent should list agentmemory's tools. With the server running you should see 16 tools (e.g. `memory_save`, `memory_smart_search`, `memory_sessions`).
105
 
106
- If you see 0 tools or an error, check that `python src/app.py` is running and `AGENTMEMORY_URL` points at it.
107
 
108
  ## 6. Setting up agent hooks
109
 
110
- Agent hooks post observations to agentmemory automatically on every tool use, command, or edit — no manual calls required. Hook scripts live in the [`hooks/`](hooks/) directory.
111
 
112
  ### Claude Code (`hooks/claude-code-hook.sh`)
113
 
114
- Add a `PostToolUse` hook to `.claude/settings.json`. The hook script reads `AGENTMEMORY_URL` and `AGENTMEMORY_SECRET` from your shell environment, so no secrets are embedded in the config file.
115
 
116
  ```json
117
  {
@@ -134,8 +134,8 @@ Add a `PostToolUse` hook to `.claude/settings.json`. The hook script reads `AGEN
134
  Set the required environment variables before starting Claude Code:
135
 
136
  ```bash
137
- export AGENTMEMORY_URL=http://127.0.0.1:3111
138
- export AGENTMEMORY_SECRET=your-secret-here # omit if no auth set
139
  ```
140
 
141
  The script picks up `$PWD` as `folderPath` and `$CLAUDE_AGENT_ID` (falling back to `"claude-code"`) as `agentId`.
@@ -151,7 +151,7 @@ const { logObservation } = require('/path/to/hooks/cursor-hook.js');
151
  logObservation(`Tool: ${toolName}\nInput: ${JSON.stringify(toolInput)}`);
152
  ```
153
 
154
- The module reads `AGENTMEMORY_URL`, `AGENTMEMORY_SECRET`, and `AGENTMEMORY_AGENT_ID` from `process.env`. Set them in your shell profile or in Cursor's environment settings.
155
 
156
  ### PowerShell terminal (`hooks/powershell-hook.ps1`)
157
 
@@ -164,24 +164,24 @@ Add a single dot-source line to your PowerShell `$PROFILE` to activate automatic
164
  Set the required variables in `$PROFILE` before the dot-source line:
165
 
166
  ```powershell
167
- $env:AGENTMEMORY_URL = "http://127.0.0.1:3111"
168
- $env:AGENTMEMORY_SECRET = "your-secret-here" # omit if no auth set
169
- $env:AGENTMEMORY_AGENT_ID = "powershell"
170
  ```
171
 
172
- The hook installs a PSReadLine `CommandValidationHandler` that fires a background job on every command you run. If PSReadLine is not available, call `Send-AgentMemoryObservation -Text "..."` manually.
173
 
174
  ### `.env` file format
175
 
176
- All hooks and the server itself read credentials from `~/.agentmemory/.env`. Create this file if it doesn't exist:
177
 
178
  ```
179
  III_REST_PORT=3111
180
- AGENTMEMORY_SECRET=your-secret-here
181
  GEMINI_API_KEY=your-gemini-key-here
182
  ```
183
 
184
- The server loads this file on startup. Hook scripts read the same variables from your shell environment (export them from your profile after sourcing `~/.agentmemory/.env`, or use `direnv` / `dotenv` tooling).
185
 
186
  ## 7. Open the viewer (optional)
187
 
@@ -200,8 +200,8 @@ The viewer shows live sessions, memories, and the knowledge graph.
200
  Vector search finds memories semantically, not just by keyword. Enable it with a free Gemini API key:
201
 
202
  ```bash
203
- mkdir -p ~/.agentmemory
204
- echo "GEMINI_API_KEY=your-key-here" >> ~/.agentmemory/.env
205
  ```
206
 
207
  Restart the server after adding the key. The viewer's search bar will now use hybrid BM25 + vector retrieval.
@@ -211,9 +211,9 @@ Restart the server after adding the key. The viewer's search bar will now use hy
211
  LLM compression makes each observation richer and more searchable. Requires an LLM API key:
212
 
213
  ```bash
214
- # Add to ~/.agentmemory/.env
215
  ANTHROPIC_API_KEY=your-key # or OPENAI_API_KEY or GEMINI_API_KEY
216
- AGENTMEMORY_AUTO_COMPRESS=true
217
  ```
218
 
219
  ## Lifecycle
@@ -223,11 +223,11 @@ AGENTMEMORY_AUTO_COMPRESS=true
223
  pkill -f "python src/app.py"
224
 
225
  # Check what's stored
226
- curl http://localhost:3111/agentmemory/sessions
227
- curl http://localhost:3111/agentmemory/memories
228
 
229
  # Health check
230
- curl http://localhost:3111/agentmemory/livez
231
  ```
232
 
233
  ## Troubleshooting
@@ -235,18 +235,18 @@ curl http://localhost:3111/agentmemory/livez
235
  | Symptom | Fix |
236
  |---------|-----|
237
  | `ModuleNotFoundError: No module named 'flask'` | Run `pip install -r requirements.txt` |
238
- | `Address already in use` on port 3111 | Set `III_REST_PORT=3112` in `~/.agentmemory/.env` and restart |
239
  | `/livez` never returns | Check `python src/app.py` stderr for import errors |
240
- | 0 tools in agent | Confirm `AGENTMEMORY_URL=http://localhost:3111` and the server is running |
241
- | `401 Unauthorized` | Set `AGENTMEMORY_SECRET` env var in the MCP config to match `~/.agentmemory/.env` |
242
- | Vector search not working | Add `GEMINI_API_KEY` to `~/.agentmemory/.env` and restart |
243
  | Windows Python path issue | Use `python3` instead of `python`, or use WSL2 |
244
 
245
  ## Report success
246
 
247
  Report back:
248
 
249
- - agentmemory-python server running on port 3111
250
  - MCP wired into the agent, tool count visible
251
  - Save and recall round-trip returned the probe memory
252
  - Viewer available at http://localhost:3111/viewer
 
1
+ # Install agentcache-python (agent runbook)
2
 
3
+ You are a coding agent. Follow this runbook top to bottom to install agentcache-python, wire it into yourself, and verify that persistent memory works. Run each command, check the result against the "Expect" note, then move on.
4
 
5
  ## What you are installing
6
 
7
+ agentcache-python is a local memory server for coding agents. It runs on your machine, exposes a REST API on port 3111 and an MCP endpoint, and stores all memories in a single SQLite file at `~/.agentcache/agentcache.db`. It is built with Python and Flask — no Node.js, no external database, no daemon to manage separately.
8
 
9
  Default mode works with no API key. It runs BM25 keyword search out of the box. Set `GEMINI_API_KEY` to also enable vector semantic search.
10
 
 
12
 
13
  - Python 3.10 or newer. Check with `python --version` or `python3 --version`.
14
  - pip. Check with `pip --version`.
15
+ - Port 3111 free. If taken, set `III_REST_PORT=<other>` in `~/.agentcache/.env`.
16
 
17
  ## 1. Clone the repo
18
 
19
  ```bash
20
+ git clone https://github.com/Yash030/agentcache-python.git
21
+ cd agentcache-python
22
  ```
23
 
24
  Expect: the directory exists with `src/app.py` inside.
 
44
  Wait until it is reachable:
45
 
46
  ```bash
47
+ curl -fsS http://localhost:3111/agentcache/livez
48
  ```
49
 
50
  Expect: `{"status": "ok"}`. On first run the BM25 index initializes in the background; this takes under a second.
 
53
 
54
  ```bash
55
  # Save a memory
56
+ curl -X POST http://localhost:3111/agentcache/remember \
57
  -H "Content-Type: application/json" \
58
+ -d '{"content":"agentcache-python install verification probe","concepts":["install-check"]}'
59
 
60
  # Recall it
61
+ curl -X POST http://localhost:3111/agentcache/search \
62
  -H "Content-Type: application/json" \
63
  -d '{"query":"install verification probe","limit":5}'
64
  ```
 
67
 
68
  ## 5. Wire MCP into the calling agent
69
 
70
+ Detect which agent is running this runbook, then add agentcache-python to its MCP config.
71
 
72
  ### Standard MCP block (Cursor, Claude Desktop, Cline, Windsurf, Gemini CLI)
73
 
74
  Merge this into the agent's `mcpServers` object:
75
 
76
  ```json
77
+ "agentcache": {
78
  "command": "npx",
79
+ "args": ["-y", "@agentcache/mcp"],
80
  "env": {
81
+ "AGENTCACHE_URL": "http://localhost:3111"
82
  }
83
  }
84
  ```
 
88
  Add to `~/.claude.json` under `mcpServers`:
89
 
90
  ```json
91
+ "agentcache": {
92
  "command": "npx",
93
+ "args": ["-y", "@agentcache/mcp"],
94
  "env": {
95
+ "AGENTCACHE_URL": "http://localhost:3111"
96
  }
97
  }
98
  ```
 
101
 
102
  ### Any agent — verify tool count
103
 
104
+ After wiring, the agent should list agentcache's tools. With the server running you should see 16 tools (e.g. `memory_save`, `memory_smart_search`, `memory_sessions`).
105
 
106
+ If you see 0 tools or an error, check that `python src/app.py` is running and `AGENTCACHE_URL` points at it.
107
 
108
  ## 6. Setting up agent hooks
109
 
110
+ Agent hooks post observations to agentcache automatically on every tool use, command, or edit — no manual calls required. Hook scripts live in the [`hooks/`](hooks/) directory.
111
 
112
  ### Claude Code (`hooks/claude-code-hook.sh`)
113
 
114
+ Add a `PostToolUse` hook to `.claude/settings.json`. The hook script reads `AGENTCACHE_URL` and `AGENTCACHE_SECRET` from your shell environment, so no secrets are embedded in the config file.
115
 
116
  ```json
117
  {
 
134
  Set the required environment variables before starting Claude Code:
135
 
136
  ```bash
137
+ export AGENTCACHE_URL=http://127.0.0.1:3111
138
+ export AGENTCACHE_SECRET=your-secret-here # omit if no auth set
139
  ```
140
 
141
  The script picks up `$PWD` as `folderPath` and `$CLAUDE_AGENT_ID` (falling back to `"claude-code"`) as `agentId`.
 
151
  logObservation(`Tool: ${toolName}\nInput: ${JSON.stringify(toolInput)}`);
152
  ```
153
 
154
+ The module reads `AGENTCACHE_URL`, `AGENTCACHE_SECRET`, and `AGENTCACHE_AGENT_ID` from `process.env`. Set them in your shell profile or in Cursor's environment settings.
155
 
156
  ### PowerShell terminal (`hooks/powershell-hook.ps1`)
157
 
 
164
  Set the required variables in `$PROFILE` before the dot-source line:
165
 
166
  ```powershell
167
+ $env:AGENTCACHE_URL = "http://127.0.0.1:3111"
168
+ $env:AGENTCACHE_SECRET = "your-secret-here" # omit if no auth set
169
+ $env:AGENTCACHE_AGENT_ID = "powershell"
170
  ```
171
 
172
+ The hook installs a PSReadLine `CommandValidationHandler` that fires a background job on every command you run. If PSReadLine is not available, call `Send-AgentCacheObservation -Text "..."` manually.
173
 
174
  ### `.env` file format
175
 
176
+ All hooks and the server itself read credentials from `~/.agentcache/.env`. Create this file if it doesn't exist:
177
 
178
  ```
179
  III_REST_PORT=3111
180
+ AGENTCACHE_SECRET=your-secret-here
181
  GEMINI_API_KEY=your-gemini-key-here
182
  ```
183
 
184
+ The server loads this file on startup. Hook scripts read the same variables from your shell environment (export them from your profile after sourcing `~/.agentcache/.env`, or use `direnv` / `dotenv` tooling).
185
 
186
  ## 7. Open the viewer (optional)
187
 
 
200
  Vector search finds memories semantically, not just by keyword. Enable it with a free Gemini API key:
201
 
202
  ```bash
203
+ mkdir -p ~/.agentcache
204
+ echo "GEMINI_API_KEY=your-key-here" >> ~/.agentcache/.env
205
  ```
206
 
207
  Restart the server after adding the key. The viewer's search bar will now use hybrid BM25 + vector retrieval.
 
211
  LLM compression makes each observation richer and more searchable. Requires an LLM API key:
212
 
213
  ```bash
214
+ # Add to ~/.agentcache/.env
215
  ANTHROPIC_API_KEY=your-key # or OPENAI_API_KEY or GEMINI_API_KEY
216
+ AGENTCACHE_AUTO_COMPRESS=true
217
  ```
218
 
219
  ## Lifecycle
 
223
  pkill -f "python src/app.py"
224
 
225
  # Check what's stored
226
+ curl http://localhost:3111/agentcache/sessions
227
+ curl http://localhost:3111/agentcache/memories
228
 
229
  # Health check
230
+ curl http://localhost:3111/agentcache/livez
231
  ```
232
 
233
  ## Troubleshooting
 
235
  | Symptom | Fix |
236
  |---------|-----|
237
  | `ModuleNotFoundError: No module named 'flask'` | Run `pip install -r requirements.txt` |
238
+ | `Address already in use` on port 3111 | Set `III_REST_PORT=3112` in `~/.agentcache/.env` and restart |
239
  | `/livez` never returns | Check `python src/app.py` stderr for import errors |
240
+ | 0 tools in agent | Confirm `AGENTCACHE_URL=http://localhost:3111` and the server is running |
241
+ | `401 Unauthorized` | Set `AGENTCACHE_SECRET` env var in the MCP config to match `~/.agentcache/.env` |
242
+ | Vector search not working | Add `GEMINI_API_KEY` to `~/.agentcache/.env` and restart |
243
  | Windows Python path issue | Use `python3` instead of `python`, or use WSL2 |
244
 
245
  ## Report success
246
 
247
  Report back:
248
 
249
+ - agentcache-python server running on port 3111
250
  - MCP wired into the agent, tool count visible
251
  - Save and recall round-trip returned the probe memory
252
  - Viewer available at http://localhost:3111/viewer
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: AgentMemory Python
3
  emoji: 🧠
4
  colorFrom: blue
5
  colorTo: indigo
@@ -7,7 +7,7 @@ sdk: docker
7
  pinned: false
8
  ---
9
 
10
- <h1 align="center">agentmemory-python</h1>
11
 
12
  <p align="center">
13
  <strong>Persistent memory for AI coding agents — pure Python, zero external databases.</strong><br/>
@@ -49,14 +49,14 @@ pinned: false
49
 
50
  ## What Is This?
51
 
52
- **agentmemory-python** is a Python reimplementation of the [agentmemory](https://github.com/rohitg00/agentmemory) persistent memory server. It exposes a REST API, WebSocket stream, and MCP tools endpoint that AI coding agents use to store and retrieve session observations, long-term memories, lessons, and pinned memory slots.
53
 
54
  Key differences from the Node.js original:
55
 
56
  - **No Node.js or iii-engine** — runs with plain `python src/app.py`
57
  - **SQLite instead of Dolt** — single file, WAL mode, instant startup
58
  - **HuggingFace Space ready** — deploys in one click, data synced to an HF dataset repo
59
- - **Same REST + MCP wire format** — drop-in for any agent already wired to agentmemory
60
 
61
  Your agent captures every tool call, stores them as observations, compresses them into searchable memory, and injects the right context at the start of every new session — automatically.
62
 
@@ -68,8 +68,8 @@ Your agent captures every tool call, stores them as observations, compresses the
68
 
69
  ```bash
70
  # Clone
71
- git clone https://github.com/Yash030/agentmemory-python.git
72
- cd agentmemory-python
73
 
74
  # Install dependencies (no build step)
75
  pip install -r requirements.txt
@@ -84,16 +84,16 @@ Server starts on **http://localhost:3111**. Open the viewer at http://localhost:
84
 
85
  ```bash
86
  # Health check
87
- curl http://localhost:3111/agentmemory/livez
88
  # {"status": "ok"}
89
 
90
  # Save a memory
91
- curl -X POST http://localhost:3111/agentmemory/remember \
92
  -H "Content-Type: application/json" \
93
  -d '{"content": "JWT auth uses jose middleware in src/middleware/auth.ts", "concepts": ["auth", "jwt"]}'
94
 
95
  # Recall it
96
- curl -X POST http://localhost:3111/agentmemory/search \
97
  -H "Content-Type: application/json" \
98
  -d '{"query": "authentication middleware", "limit": 5}'
99
  ```
@@ -112,7 +112,7 @@ curl -X POST http://localhost:3111/agentmemory/search \
112
  | Hybrid BM25 + vector search | ✅ | Requires `GEMINI_API_KEY` |
113
  | 4-tier memory consolidation | ⚙️ | `CONSOLIDATION_ENABLED=true` + LLM key |
114
  | Knowledge graph extraction | ⚙️ | `GRAPH_EXTRACTION_ENABLED=true` + LLM key |
115
- | LLM observation compression | ⚙️ | `AGENTMEMORY_AUTO_COMPRESS=true` + LLM key |
116
  | Lessons with confidence decay | ✅ | Fingerprinted, auto-strengthen on repeat |
117
  | Memory slots (pinned context) | ✅ | CRUD + auto-reflect |
118
  | Session replay | ✅ | Full timeline in viewer |
@@ -135,18 +135,18 @@ Inspired by how human memory works — raw experience → compressed episodes
135
 
136
  ## MCP Integration
137
 
138
- Wire agentmemory-python into your agent's MCP config. It speaks the same MCP protocol as the Node.js original.
139
 
140
  ### Most agents (Cursor, Claude Desktop, Cline, Windsurf)
141
 
142
  ```json
143
  {
144
  "mcpServers": {
145
- "agentmemory": {
146
  "command": "npx",
147
- "args": ["-y", "@agentmemory/mcp"],
148
  "env": {
149
- "AGENTMEMORY_URL": "http://localhost:3111"
150
  }
151
  }
152
  }
@@ -158,16 +158,16 @@ Wire agentmemory-python into your agent's MCP config. It speaks the same MCP pro
158
  Paste this prompt and your agent will wire everything:
159
 
160
  ```
161
- Start agentmemory-python: run `python src/app.py` from the agentmemory-python directory.
162
  Then add this MCP server to ~/.claude.json under mcpServers:
163
  {
164
- "agentmemory": {
165
  "command": "npx",
166
- "args": ["-y", "@agentmemory/mcp"],
167
- "env": { "AGENTMEMORY_URL": "http://localhost:3111" }
168
  }
169
  }
170
- Verify with: curl http://localhost:3111/agentmemory/livez
171
  Open the viewer at: http://localhost:3111/viewer
172
  ```
173
 
@@ -193,7 +193,7 @@ Open the viewer at: http://localhost:3111/viewer
193
  | `memory_forget` | Delete memory, session, or observations |
194
  | `memory_export` | Export all memory data as JSON |
195
  | `agent_observe` | Log agent execution observation |
196
- | `agent_remember` | Save agent memory to long-term storage |
197
  | `memory_antigravity_sync` | Sync Antigravity transcripts to memory |
198
  | `memory_antigravity_sync_all` | Master sync: transcript + crystallize + reflect |
199
  | `memory_slot_list` | List all pinned memory slots |
@@ -211,7 +211,7 @@ Open the viewer at: http://localhost:3111/viewer
211
 
212
  ## API Reference
213
 
214
- Base URL: `http://localhost:3111/agentmemory`
215
 
216
  ### Health
217
 
@@ -304,7 +304,7 @@ Base URL: `http://localhost:3111/agentmemory`
304
 
305
  ## Configuration
306
 
307
- Create `~/.agentmemory/.env` (no `export` prefix needed):
308
 
309
  ```env
310
  # Server port
@@ -322,22 +322,22 @@ ANTHROPIC_API_KEY=your-anthropic-key
322
  # LLM-powered features (disabled by default — spend tokens)
323
  CONSOLIDATION_ENABLED=true
324
  GRAPH_EXTRACTION_ENABLED=true
325
- AGENTMEMORY_AUTO_COMPRESS=true
326
 
327
  # Context injection limits
328
  TOKEN_BUDGET=2000
329
  MAX_OBS_PER_SESSION=500
330
 
331
  # Auth — set to require Bearer token on all endpoints
332
- AGENTMEMORY_SECRET=your-secret
333
 
334
  # Agent scope isolation
335
  AGENT_ID=my-agent
336
- AGENTMEMORY_AGENT_SCOPE=isolated # only see this agent's data
337
 
338
  # HuggingFace sync
339
  HF_TOKEN=your-hf-token
340
- AGENTMEMORY_DATASET_REPO=username/agentmemory-data
341
  ```
342
 
343
  ### Full Variable Reference
@@ -346,14 +346,14 @@ AGENTMEMORY_DATASET_REPO=username/agentmemory-data
346
  |----------|---------|---------|
347
  | `III_REST_PORT` / `PORT` | `3111` | API server port |
348
  | `GEMINI_API_KEY` / `GOOGLE_API_KEY` | — | Enables 768-dim vector search |
349
- | `AGENTMEMORY_SECRET` | — | Bearer token auth on all endpoints |
350
  | `AGENT_ID` | — | Default agent ID for scope isolation |
351
- | `AGENTMEMORY_AGENT_SCOPE=isolated` | — | Filters data to current `AGENT_ID` |
352
  | `MAX_OBS_PER_SESSION` | `500` | Hard cap on observations per session |
353
  | `TOKEN_BUDGET` | `2000` | Max tokens in compiled context |
354
  | `GRAPH_EXTRACTION_ENABLED` | `false` | Knowledge graph (needs LLM) |
355
  | `CONSOLIDATION_ENABLED` | `false` | Memory consolidation (needs LLM) |
356
- | `AGENTMEMORY_AUTO_COMPRESS` | `false` | LLM observation compression |
357
 
358
  ---
359
 
@@ -381,17 +381,17 @@ This project is designed to run as a HuggingFace Space. Data is stored in an HF
381
  ### Setup
382
 
383
  1. Fork this repo as a HuggingFace Space (SDK: Docker)
384
- 2. Create a dataset repo (e.g. `your-username/agentmemory-data`)
385
  3. Add Space secrets in the HF dashboard:
386
 
387
  | Secret | Value |
388
  |--------|-------|
389
  | `HF_TOKEN` | Your HF write token |
390
- | `AGENTMEMORY_DATASET_REPO` | `your-username/agentmemory-data` |
391
- | `AGENTMEMORY_SECRET` | A random secret (optional but recommended) |
392
  | `GEMINI_API_KEY` | Gemini key (optional, enables vector search) |
393
 
394
- 4. The Space boots, restores `agentmemory.db` from the dataset repo, and starts the server
395
 
396
  ### How sync works
397
 
@@ -403,7 +403,7 @@ python sync.py
403
 
404
  # Environment for sync
405
  HF_TOKEN=...
406
- AGENTMEMORY_DATASET_REPO=username/agentmemory-data
407
  ```
408
 
409
  ---
@@ -411,7 +411,7 @@ AGENTMEMORY_DATASET_REPO=username/agentmemory-data
411
  ## Architecture
412
 
413
  ```
414
- agentmemory-python/
415
  ├── src/
416
  │ ├── app.py Flask server — all endpoints, WebSocket broadcaster
417
  │ ├── db.py SQLite StateKV — WAL mode, audit_log table
@@ -427,7 +427,7 @@ agentmemory-python/
427
 
428
  ### Database layout
429
 
430
- Two SQLite tables in `~/.agentmemory/agentmemory.db`:
431
 
432
  ```sql
433
  -- All data lives here, namespaced by scope
@@ -460,9 +460,9 @@ Query
460
 
461
  ---
462
 
463
- ## vs Original agentmemory
464
 
465
- | | agentmemory (Node.js) | agentmemory-python |
466
  |---|---|---|
467
  | Runtime | Node.js 20+ | Python 3.10+ |
468
  | Storage | Dolt SQL (git-versioned MySQL) | SQLite WAL (single file) |
@@ -473,7 +473,7 @@ Query
473
  | Deploy | npm, Docker, fly.io, Railway, Render | Docker, HuggingFace Spaces |
474
  | Cold boot | ~7s (iii engine warm-up) | <2s |
475
  | Database size | ~232MB (417 Dolt chunk files) | ~20MB (single `.db` file) |
476
- | Setup | `npm install -g @agentmemory/agentmemory` | `pip install -r requirements.txt` |
477
 
478
  Choose the Python version for: simpler setup, HF Space deployment, single-file database, no Node.js, or Python ecosystem integration.
479
 
 
1
  ---
2
+ title: AgentCache Python
3
  emoji: 🧠
4
  colorFrom: blue
5
  colorTo: indigo
 
7
  pinned: false
8
  ---
9
 
10
+ <h1 align="center">agentcache-python</h1>
11
 
12
  <p align="center">
13
  <strong>Persistent memory for AI coding agents — pure Python, zero external databases.</strong><br/>
 
49
 
50
  ## What Is This?
51
 
52
+ **agentcache-python** is a Python reimplementation of the [agentcache](https://github.com/rohitg00/agentcache) persistent memory server. It exposes a REST API, WebSocket stream, and MCP tools endpoint that AI coding agents use to store and retrieve session observations, long-term memories, lessons, and pinned memory slots.
53
 
54
  Key differences from the Node.js original:
55
 
56
  - **No Node.js or iii-engine** — runs with plain `python src/app.py`
57
  - **SQLite instead of Dolt** — single file, WAL mode, instant startup
58
  - **HuggingFace Space ready** — deploys in one click, data synced to an HF dataset repo
59
+ - **Same REST + MCP wire format** — drop-in for any agent already wired to agentcache
60
 
61
  Your agent captures every tool call, stores them as observations, compresses them into searchable memory, and injects the right context at the start of every new session — automatically.
62
 
 
68
 
69
  ```bash
70
  # Clone
71
+ git clone https://github.com/Yash030/agentcache-python.git
72
+ cd agentcache-python
73
 
74
  # Install dependencies (no build step)
75
  pip install -r requirements.txt
 
84
 
85
  ```bash
86
  # Health check
87
+ curl http://localhost:3111/agentcache/livez
88
  # {"status": "ok"}
89
 
90
  # Save a memory
91
+ curl -X POST http://localhost:3111/agentcache/remember \
92
  -H "Content-Type: application/json" \
93
  -d '{"content": "JWT auth uses jose middleware in src/middleware/auth.ts", "concepts": ["auth", "jwt"]}'
94
 
95
  # Recall it
96
+ curl -X POST http://localhost:3111/agentcache/search \
97
  -H "Content-Type: application/json" \
98
  -d '{"query": "authentication middleware", "limit": 5}'
99
  ```
 
112
  | Hybrid BM25 + vector search | ✅ | Requires `GEMINI_API_KEY` |
113
  | 4-tier memory consolidation | ⚙️ | `CONSOLIDATION_ENABLED=true` + LLM key |
114
  | Knowledge graph extraction | ⚙️ | `GRAPH_EXTRACTION_ENABLED=true` + LLM key |
115
+ | LLM observation compression | ⚙️ | `AGENTCACHE_AUTO_COMPRESS=true` + LLM key |
116
  | Lessons with confidence decay | ✅ | Fingerprinted, auto-strengthen on repeat |
117
  | Memory slots (pinned context) | ✅ | CRUD + auto-reflect |
118
  | Session replay | ✅ | Full timeline in viewer |
 
135
 
136
  ## MCP Integration
137
 
138
+ Wire agentcache-python into your agent's MCP config. It speaks the same MCP protocol as the Node.js original.
139
 
140
  ### Most agents (Cursor, Claude Desktop, Cline, Windsurf)
141
 
142
  ```json
143
  {
144
  "mcpServers": {
145
+ "agentcache": {
146
  "command": "npx",
147
+ "args": ["-y", "@agentcache/mcp"],
148
  "env": {
149
+ "AGENTCACHE_URL": "http://localhost:3111"
150
  }
151
  }
152
  }
 
158
  Paste this prompt and your agent will wire everything:
159
 
160
  ```
161
+ Start agentcache-python: run `python src/app.py` from the agentcache-python directory.
162
  Then add this MCP server to ~/.claude.json under mcpServers:
163
  {
164
+ "agentcache": {
165
  "command": "npx",
166
+ "args": ["-y", "@agentcache/mcp"],
167
+ "env": { "AGENTCACHE_URL": "http://localhost:3111" }
168
  }
169
  }
170
+ Verify with: curl http://localhost:3111/agentcache/livez
171
  Open the viewer at: http://localhost:3111/viewer
172
  ```
173
 
 
193
  | `memory_forget` | Delete memory, session, or observations |
194
  | `memory_export` | Export all memory data as JSON |
195
  | `agent_observe` | Log agent execution observation |
196
+ | `agent_remember` | Save agent cache to long-term storage |
197
  | `memory_antigravity_sync` | Sync Antigravity transcripts to memory |
198
  | `memory_antigravity_sync_all` | Master sync: transcript + crystallize + reflect |
199
  | `memory_slot_list` | List all pinned memory slots |
 
211
 
212
  ## API Reference
213
 
214
+ Base URL: `http://localhost:3111/agentcache`
215
 
216
  ### Health
217
 
 
304
 
305
  ## Configuration
306
 
307
+ Create `~/.agentcache/.env` (no `export` prefix needed):
308
 
309
  ```env
310
  # Server port
 
322
  # LLM-powered features (disabled by default — spend tokens)
323
  CONSOLIDATION_ENABLED=true
324
  GRAPH_EXTRACTION_ENABLED=true
325
+ AGENTCACHE_AUTO_COMPRESS=true
326
 
327
  # Context injection limits
328
  TOKEN_BUDGET=2000
329
  MAX_OBS_PER_SESSION=500
330
 
331
  # Auth — set to require Bearer token on all endpoints
332
+ AGENTCACHE_SECRET=your-secret
333
 
334
  # Agent scope isolation
335
  AGENT_ID=my-agent
336
+ AGENTCACHE_AGENT_SCOPE=isolated # only see this agent's data
337
 
338
  # HuggingFace sync
339
  HF_TOKEN=your-hf-token
340
+ AGENTCACHE_DATASET_REPO=username/agentcache-data
341
  ```
342
 
343
  ### Full Variable Reference
 
346
  |----------|---------|---------|
347
  | `III_REST_PORT` / `PORT` | `3111` | API server port |
348
  | `GEMINI_API_KEY` / `GOOGLE_API_KEY` | — | Enables 768-dim vector search |
349
+ | `AGENTCACHE_SECRET` | — | Bearer token auth on all endpoints |
350
  | `AGENT_ID` | — | Default agent ID for scope isolation |
351
+ | `AGENTCACHE_AGENT_SCOPE=isolated` | — | Filters data to current `AGENT_ID` |
352
  | `MAX_OBS_PER_SESSION` | `500` | Hard cap on observations per session |
353
  | `TOKEN_BUDGET` | `2000` | Max tokens in compiled context |
354
  | `GRAPH_EXTRACTION_ENABLED` | `false` | Knowledge graph (needs LLM) |
355
  | `CONSOLIDATION_ENABLED` | `false` | Memory consolidation (needs LLM) |
356
+ | `AGENTCACHE_AUTO_COMPRESS` | `false` | LLM observation compression |
357
 
358
  ---
359
 
 
381
  ### Setup
382
 
383
  1. Fork this repo as a HuggingFace Space (SDK: Docker)
384
+ 2. Create a dataset repo (e.g. `your-username/agentcache-data`)
385
  3. Add Space secrets in the HF dashboard:
386
 
387
  | Secret | Value |
388
  |--------|-------|
389
  | `HF_TOKEN` | Your HF write token |
390
+ | `AGENTCACHE_DATASET_REPO` | `your-username/agentcache-data` |
391
+ | `AGENTCACHE_SECRET` | A random secret (optional but recommended) |
392
  | `GEMINI_API_KEY` | Gemini key (optional, enables vector search) |
393
 
394
+ 4. The Space boots, restores `agentcache.db` from the dataset repo, and starts the server
395
 
396
  ### How sync works
397
 
 
403
 
404
  # Environment for sync
405
  HF_TOKEN=...
406
+ AGENTCACHE_DATASET_REPO=username/agentcache-data
407
  ```
408
 
409
  ---
 
411
  ## Architecture
412
 
413
  ```
414
+ agentcache-python/
415
  ├── src/
416
  │ ├── app.py Flask server — all endpoints, WebSocket broadcaster
417
  │ ├── db.py SQLite StateKV — WAL mode, audit_log table
 
427
 
428
  ### Database layout
429
 
430
+ Two SQLite tables in `~/.agentcache/agentcache.db`:
431
 
432
  ```sql
433
  -- All data lives here, namespaced by scope
 
460
 
461
  ---
462
 
463
+ ## vs Original agentcache
464
 
465
+ | | agentcache (Node.js) | agentcache-python |
466
  |---|---|---|
467
  | Runtime | Node.js 20+ | Python 3.10+ |
468
  | Storage | Dolt SQL (git-versioned MySQL) | SQLite WAL (single file) |
 
473
  | Deploy | npm, Docker, fly.io, Railway, Render | Docker, HuggingFace Spaces |
474
  | Cold boot | ~7s (iii engine warm-up) | <2s |
475
  | Database size | ~232MB (417 Dolt chunk files) | ~20MB (single `.db` file) |
476
+ | Setup | `npm install -g @agentcache/agentcache` | `pip install -r requirements.txt` |
477
 
478
  Choose the Python version for: simpler setup, HF Space deployment, single-file database, no Node.js, or Python ecosystem integration.
479
 
agentmemory.md → agentcache.md RENAMED
@@ -1,4 +1,4 @@
1
- # AgentMemory Automation & Client Hook Roundabout
2
 
3
  This file documents the technical details and configurations for automating memory sessions, observations, and prompt-context syncing in environments that lack native client-side hook runners.
4
 
@@ -9,7 +9,7 @@ For standard command shells that do not have custom plugins (such as PowerShell)
9
  * **Profile File**: `D:\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1`
10
  * **Automation Mechanism**:
11
  1. **Prompt Redefinition**: Overrides the default PowerShell `prompt` function to retrieve the last executed command from the session history (`Get-History`).
12
- 2. **Asynchronous Dispatch**: Uses PowerShell's `Start-Job` to send the command, execution status, and duration to the Hugging Face Space database (`https://yash030-agentmemory-python.hf.space/agentmemory/observe`) in the background, keeping terminal prompts lag-free.
13
  3. **Interactive Shield**: Wraps the prompts and startup session creation in `if (-not [Console]::IsInputRedirected)` so background script executions or tool runners do not pollute the database.
14
  4. **Active Session Reuse**: On console startup, checks for a cached session ID in `current_session.txt` and reuses it to prevent session clutter.
15
 
 
1
+ # AgentCache Automation & Client Hook Roundabout
2
 
3
  This file documents the technical details and configurations for automating memory sessions, observations, and prompt-context syncing in environments that lack native client-side hook runners.
4
 
 
9
  * **Profile File**: `D:\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1`
10
  * **Automation Mechanism**:
11
  1. **Prompt Redefinition**: Overrides the default PowerShell `prompt` function to retrieve the last executed command from the session history (`Get-History`).
12
+ 2. **Asynchronous Dispatch**: Uses PowerShell's `Start-Job` to send the command, execution status, and duration to the Hugging Face Space database (`https://yash030-agentcache-python.hf.space/agentcache/observe`) in the background, keeping terminal prompts lag-free.
13
  3. **Interactive Shield**: Wraps the prompts and startup session creation in `if (-not [Console]::IsInputRedirected)` so background script executions or tool runners do not pollute the database.
14
  4. **Active Session Reuse**: On console startup, checks for a cached session ID in `current_session.txt` and reuses it to prevent session clutter.
15
 
docker-compose.yml CHANGED
@@ -22,8 +22,8 @@ services:
22
  # refactored to register as a sandboxed worker.
23
  #
24
  # Override per-shell or via .env file:
25
- # AGENTMEMORY_III_VERSION=0.11.7 docker compose up
26
- image: iiidev/iii:${AGENTMEMORY_III_VERSION:-0.11.2}
27
  user: "65532:65532"
28
  depends_on:
29
  iii-init:
 
22
  # refactored to register as a sandboxed worker.
23
  #
24
  # Override per-shell or via .env file:
25
+ # AGENTCACHE_III_VERSION=0.11.7 docker compose up
26
+ image: iiidev/iii:${AGENTCACHE_III_VERSION:-${AGENTMEMORY_III_VERSION:-0.11.2}}
27
  user: "65532:65532"
28
  depends_on:
29
  iii-init:
pyproject.toml CHANGED
@@ -3,9 +3,9 @@ requires = ["setuptools>=68", "wheel"]
3
  build-backend = "setuptools.backends.legacy:build"
4
 
5
  [project]
6
- name = "agentmemory"
7
  version = "0.9.8"
8
- description = "A Python REST + WebSocket + MCP memory server for AI agents, backed by SQLite"
9
  readme = "README.md"
10
  license = { text = "MIT" }
11
  requires-python = ">=3.10"
@@ -28,7 +28,7 @@ local-embeddings = [
28
  ]
29
 
30
  [project.scripts]
31
- agentmemory = "src.cli:main"
32
 
33
  [tool.setuptools.packages.find]
34
  where = ["."]
 
3
  build-backend = "setuptools.backends.legacy:build"
4
 
5
  [project]
6
+ name = "agentcache"
7
  version = "0.9.8"
8
+ description = "A Python REST + WebSocket + MCP cache server for AI agents, backed by SQLite"
9
  readme = "README.md"
10
  license = { text = "MIT" }
11
  requires-python = ">=3.10"
 
28
  ]
29
 
30
  [project.scripts]
31
+ agentcache = "src.cli:main"
32
 
33
  [tool.setuptools.packages.find]
34
  where = ["."]
src/app.py CHANGED
@@ -18,7 +18,9 @@ if __name__ == "__main__":
18
 
19
 
20
  def _load_env() -> None:
21
- env_path = os.path.join(os.path.expanduser("~"), ".agentmemory", ".env")
 
 
22
  if not os.path.exists(env_path):
23
  return
24
  try:
@@ -47,8 +49,8 @@ def create_app() -> Flask:
47
  global kv, embedding_provider, persistence
48
 
49
  # Check security credentials
50
- if not os.getenv("AGENTMEMORY_SECRET"):
51
- print("[security] WARNING: AGENTMEMORY_SECRET is not set! All API endpoints are publicly accessible without authentication.")
52
 
53
  import search as search_mod
54
  import functions
@@ -59,10 +61,10 @@ def create_app() -> Flask:
59
  kv = StateKV()
60
 
61
  # 2. Embedding provider — auto-select by priority (D5.3):
62
- # GEMINI_API_KEY → OPENAI_API_KEY → AGENTMEMORY_LOCAL_EMBEDDING_MODEL → BM25-only
63
  api_key = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
64
  openai_key = os.getenv("OPENAI_API_KEY")
65
- local_model = os.getenv("AGENTMEMORY_LOCAL_EMBEDDING_MODEL")
66
 
67
  if api_key:
68
  try:
@@ -118,7 +120,7 @@ def create_app() -> Flask:
118
 
119
  @sock.route("/stream/mem-live/viewer")
120
  def stream_viewer(ws):
121
- secret = os.getenv("AGENTMEMORY_SECRET")
122
  if secret:
123
  token = request.args.get("token") or request.args.get("secret")
124
  if not token or not hmac.compare_digest(
@@ -150,6 +152,7 @@ def create_app() -> Flask:
150
 
151
  @flask_app.route("/")
152
  @flask_app.route("/viewer")
 
153
  @flask_app.route("/agentmemory/viewer")
154
  def serve_viewer():
155
  try:
@@ -161,7 +164,7 @@ def create_app() -> Flask:
161
  def serve_favicon():
162
  return send_from_directory(os.path.join(_base_dir, "viewer"), "favicon.svg")
163
 
164
- # 7. CORS after_request — D2.1: configurable via AGENTMEMORY_CORS_ORIGINS env var
165
  # Default allows localhost, 127.0.0.1, HuggingFace Spaces, vscode-webview://, chrome-extension://
166
  # Wildcard entries like "*.hf.space" match any subdomain via suffix check.
167
  _default_cors = (
@@ -169,7 +172,7 @@ def create_app() -> Flask:
169
  "https://huggingface.co,https://*.hf.space,"
170
  "vscode-webview://*,chrome-extension://*"
171
  )
172
- _cors_origins_raw = os.getenv("AGENTMEMORY_CORS_ORIGINS", _default_cors)
173
 
174
  def _parse_cors_origins(raw: str):
175
  """Return (exact_set, suffix_list) for efficient origin matching."""
 
18
 
19
 
20
  def _load_env() -> None:
21
+ env_path = os.path.join(os.path.expanduser("~"), ".agentcache", ".env")
22
+ if not os.path.exists(env_path):
23
+ env_path = os.path.join(os.path.expanduser("~"), ".agentmemory", ".env")
24
  if not os.path.exists(env_path):
25
  return
26
  try:
 
49
  global kv, embedding_provider, persistence
50
 
51
  # Check security credentials
52
+ if not os.getenv("AGENTCACHE_SECRET") and not os.getenv("AGENTMEMORY_SECRET"):
53
+ print("[security] WARNING: AGENTCACHE_SECRET/AGENTMEMORY_SECRET is not set! All API endpoints are publicly accessible without authentication.")
54
 
55
  import search as search_mod
56
  import functions
 
61
  kv = StateKV()
62
 
63
  # 2. Embedding provider — auto-select by priority (D5.3):
64
+ # GEMINI_API_KEY → OPENAI_API_KEY → AGENTCACHE_LOCAL_EMBEDDING_MODEL → BM25-only
65
  api_key = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
66
  openai_key = os.getenv("OPENAI_API_KEY")
67
+ local_model = os.getenv("AGENTCACHE_LOCAL_EMBEDDING_MODEL") or os.getenv("AGENTMEMORY_LOCAL_EMBEDDING_MODEL")
68
 
69
  if api_key:
70
  try:
 
120
 
121
  @sock.route("/stream/mem-live/viewer")
122
  def stream_viewer(ws):
123
+ secret = os.getenv("AGENTCACHE_SECRET") or os.getenv("AGENTMEMORY_SECRET")
124
  if secret:
125
  token = request.args.get("token") or request.args.get("secret")
126
  if not token or not hmac.compare_digest(
 
152
 
153
  @flask_app.route("/")
154
  @flask_app.route("/viewer")
155
+ @flask_app.route("/agentcache/viewer")
156
  @flask_app.route("/agentmemory/viewer")
157
  def serve_viewer():
158
  try:
 
164
  def serve_favicon():
165
  return send_from_directory(os.path.join(_base_dir, "viewer"), "favicon.svg")
166
 
167
+ # 7. CORS after_request — D2.1: configurable via AGENTCACHE_CORS_ORIGINS env var
168
  # Default allows localhost, 127.0.0.1, HuggingFace Spaces, vscode-webview://, chrome-extension://
169
  # Wildcard entries like "*.hf.space" match any subdomain via suffix check.
170
  _default_cors = (
 
172
  "https://huggingface.co,https://*.hf.space,"
173
  "vscode-webview://*,chrome-extension://*"
174
  )
175
+ _cors_origins_raw = os.getenv("AGENTCACHE_CORS_ORIGINS") or os.getenv("AGENTMEMORY_CORS_ORIGINS") or _default_cors
176
 
177
  def _parse_cors_origins(raw: str):
178
  """Return (exact_set, suffix_list) for efficient origin matching."""
src/{memory → cache}/__init__.py RENAMED
@@ -1,5 +1,5 @@
1
  """
2
- src/memory/ — Core business logic package for agentmemory-python.
3
 
4
  Sub-modules:
5
  observe — folder_observe(), observe(), build_synthetic_compression(), strip_private_data()
 
1
  """
2
+ src/cache/ — Core business logic package for agentcache-python.
3
 
4
  Sub-modules:
5
  observe — folder_observe(), observe(), build_synthetic_compression(), strip_private_data()
src/{memory → cache}/context.py RENAMED
File without changes
src/{memory → cache}/graph.py RENAMED
File without changes
src/{memory → cache}/health.py RENAMED
File without changes
src/{memory → cache}/observe.py RENAMED
File without changes
src/{memory → cache}/remember.py RENAMED
File without changes
src/{memory → cache}/timeline.py RENAMED
File without changes
src/cli.py CHANGED
@@ -1,11 +1,11 @@
1
  #!/usr/bin/env python3
2
  """
3
- agentmemory CLI entrypoint.
4
 
5
  Commands:
6
- agentmemory serve [--port PORT] [--host HOST]
7
- agentmemory migrate [--dry-run]
8
- agentmemory export [--output FILE]
9
  """
10
 
11
  import argparse
@@ -20,7 +20,7 @@ def cmd_serve(args) -> None:
20
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
21
  from app import create_app
22
  flask_app = create_app()
23
- print(f"[cli] Starting agentmemory on {args.host}:{args.port}")
24
  flask_app.run(host=args.host, port=args.port, debug=False)
25
 
26
 
@@ -67,13 +67,13 @@ def cmd_export(args) -> None:
67
 
68
  def main() -> None:
69
  parser = argparse.ArgumentParser(
70
- prog="agentmemory",
71
- description="agentmemory — AI agent memory server",
72
  )
73
  subparsers = parser.add_subparsers(dest="command", required=True)
74
 
75
  # serve
76
- serve_parser = subparsers.add_parser("serve", help="Start the memory server")
77
  serve_parser.add_argument("--port", type=int, default=int(os.getenv("III_REST_PORT", "3111")))
78
  serve_parser.add_argument("--host", default="0.0.0.0")
79
 
 
1
  #!/usr/bin/env python3
2
  """
3
+ agentcache CLI entrypoint.
4
 
5
  Commands:
6
+ agentcache serve [--port PORT] [--host HOST]
7
+ agentcache migrate [--dry-run]
8
+ agentcache export [--output FILE]
9
  """
10
 
11
  import argparse
 
20
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
21
  from app import create_app
22
  flask_app = create_app()
23
+ print(f"[cli] Starting agentcache on {args.host}:{args.port}")
24
  flask_app.run(host=args.host, port=args.port, debug=False)
25
 
26
 
 
67
 
68
  def main() -> None:
69
  parser = argparse.ArgumentParser(
70
+ prog="agentcache",
71
+ description="agentcache — AI agent cache server",
72
  )
73
  subparsers = parser.add_subparsers(dest="command", required=True)
74
 
75
  # serve
76
+ serve_parser = subparsers.add_parser("serve", help="Start the cache server")
77
  serve_parser.add_argument("--port", type=int, default=int(os.getenv("III_REST_PORT", "3111")))
78
  serve_parser.add_argument("--host", default="0.0.0.0")
79
 
src/connect.py CHANGED
@@ -124,22 +124,22 @@ class ClaudeCodeAdapter:
124
  next_cfg = existing.copy()
125
  servers = next_cfg.get("mcpServers", {})
126
 
127
- already_has = "agentmemory" in servers
128
  if already_has and not args.force:
129
  print(f"[OK] Claude Code already wired in {claude_json}")
130
  else:
131
  if args.dry_run:
132
- print(f"[dry-run] Would write mcpServers.agentmemory in {claude_json}")
133
  else:
134
  backup = backup_file(claude_json, "claude-code")
135
  if backup:
136
  print(f"Backed up configuration to {backup}")
137
 
138
- env = {"AGENTMEMORY_URL": os.environ.get("AGENTMEMORY_URL", "http://localhost:3111")}
139
- secret = os.environ.get("AGENTMEMORY_SECRET")
140
  if secret:
141
- env["AGENTMEMORY_SECRET"] = secret
142
- servers["agentmemory"] = {
143
  "command": sys.executable,
144
  "args": [mcp_stdio_path],
145
  "env": env
@@ -179,17 +179,17 @@ class CodexAdapter:
179
  codex_toml = os.path.join(get_home_dir(), ".codex", "config.toml")
180
  mcp_stdio_path = get_mcp_stdio_path()
181
 
182
- url = os.environ.get("AGENTMEMORY_URL", "http://localhost:3111")
183
- secret = os.environ.get("AGENTMEMORY_SECRET")
184
  toml_block = f"""
185
- [mcp_servers.agentmemory]
186
  command = "{sys.executable.replace('\\', '/')}"
187
  args = ["{mcp_stdio_path.replace('\\', '/')}"]
188
- [mcp_servers.agentmemory.env]
189
- AGENTMEMORY_URL = "{url}"
190
  """
191
  if secret:
192
- toml_block += f'AGENTMEMORY_SECRET = "{secret}"\n'
193
 
194
  exists = os.path.exists(codex_toml)
195
  current = ""
@@ -197,12 +197,12 @@ AGENTMEMORY_URL = "{url}"
197
  with open(codex_toml, "r", encoding="utf-8") as f:
198
  current = f.read()
199
 
200
- wired = "[mcp_servers.agentmemory]" in current
201
  if wired and not args.force:
202
  print(f"[OK] Codex CLI already wired in {codex_toml}")
203
  else:
204
  if args.dry_run:
205
- print(f"[dry-run] Would write [mcp_servers.agentmemory] block to {codex_toml}")
206
  else:
207
  backup = backup_file(codex_toml, "codex", "toml")
208
  if backup:
@@ -216,10 +216,10 @@ AGENTMEMORY_URL = "{url}"
216
  skipping = False
217
  for line in lines:
218
  trimmed = line.strip()
219
- if trimmed == "[mcp_servers.agentmemory]" or trimmed == "[mcp_servers.agentmemory.env]":
220
  skipping = True
221
  continue
222
- if skipping and trimmed.startswith("[") and trimmed != "[mcp_servers.agentmemory.env]":
223
  skipping = False
224
  if not skipping:
225
  out.append(line)
@@ -258,7 +258,7 @@ class HermesAdapter:
258
  return os.path.exists(hermes_dir)
259
 
260
  def install(self, args):
261
- dest_dir = os.path.join(get_home_dir(), ".hermes", "plugins", "agentmemory")
262
  src_dir = os.path.dirname(os.path.abspath(__file__))
263
  project_root = os.path.dirname(src_dir)
264
  hermes_src = os.path.join(project_root, "integrations", "hermes")
@@ -277,14 +277,14 @@ class HermesAdapter:
277
  shutil.rmtree(dest_dir)
278
 
279
  shutil.copytree(hermes_src, dest_dir)
280
- print(f"[OK] Copied Hermes memory provider plugin to {dest_dir}")
281
  print("To finish configuration, add to ~/.hermes/config.yaml:")
282
  print(" mcp_servers:")
283
- print(" agentmemory:")
284
  print(" command: python")
285
  print(f' args: ["{get_mcp_stdio_path()}"]')
286
- print(" memory:")
287
- print(" provider: agentmemory")
288
 
289
  class AntigravityAdapter:
290
  name = "antigravity"
@@ -310,22 +310,22 @@ class AntigravityAdapter:
310
  next_cfg = existing.copy()
311
  servers = next_cfg.get("mcpServers", {})
312
 
313
- already_has = "agentmemory" in servers
314
  if already_has and not args.force:
315
  print(f"[OK] Antigravity already wired in {mcp_config_path}")
316
  else:
317
  if args.dry_run:
318
- print(f"[dry-run] Would write mcpServers.agentmemory in {mcp_config_path}")
319
  else:
320
  backup = backup_file(mcp_config_path, "antigravity")
321
  if backup:
322
  print(f"Backed up config to {backup}")
323
 
324
- env = {"AGENTMEMORY_URL": os.environ.get("AGENTMEMORY_URL", "http://localhost:3111")}
325
- secret = os.environ.get("AGENTMEMORY_SECRET")
326
  if secret:
327
- env["AGENTMEMORY_SECRET"] = secret
328
- servers["agentmemory"] = {
329
  "command": sys.executable,
330
  "args": [mcp_stdio_path],
331
  "env": env
@@ -350,22 +350,22 @@ class KiroAdapter:
350
  next_cfg = existing.copy()
351
  servers = next_cfg.get("mcpServers", {})
352
 
353
- already_has = "agentmemory" in servers
354
  if already_has and not args.force:
355
  print(f"[OK] Kiro already wired in {mcp_config_path}")
356
  else:
357
  if args.dry_run:
358
- print(f"[dry-run] Would write mcpServers.agentmemory in {mcp_config_path}")
359
  else:
360
  backup = backup_file(mcp_config_path, "kiro")
361
  if backup:
362
  print(f"Backed up config to {backup}")
363
 
364
- env = {"AGENTMEMORY_URL": os.environ.get("AGENTMEMORY_URL", "http://localhost:3111")}
365
- secret = os.environ.get("AGENTMEMORY_SECRET")
366
  if secret:
367
- env["AGENTMEMORY_SECRET"] = secret
368
- servers["agentmemory"] = {
369
  "command": sys.executable,
370
  "args": [mcp_stdio_path],
371
  "env": env
@@ -383,26 +383,26 @@ class RulesGeneratorAdapter:
383
  return True
384
 
385
  def install(self, args):
386
- rule_content = """# Agent Memory Rules
387
 
388
- This workspace is integrated with long-term semantic memory via `agentmemory-python`.
389
- You must act as your own memory manager by calling the memory MCP tools at critical boundaries.
390
 
391
  ## Rules & Workflow
392
 
393
  1. **Initial Search (Prefetch Context)**:
394
- At the start of every session or new task, immediately call `memory_smart_search` with terms related to the current objective. This retrieves past architecture patterns, preferences, bug fixes, or lessons.
395
- - Example: `memory_smart_search(query="jwt token rotation logic")`
396
 
397
  2. **Lessons & Insights Capture**:
398
  When you successfully debug a complex error, discover an undocumented requirement, or establish a convention, persist it:
399
- - Call `memory_lesson_save` to record lessons that improve your coding capabilities. Duplicate saves strengthen confidence scores.
400
- - Call `memory_save` to save long-term structural facts. Always extract 2-5 specific lowercased tags (e.g. `auth-flow`, `refresh-token`) as concepts.
401
 
402
  3. **Checklist Before Ending**:
403
  Before stating a task is complete:
404
  - Reflect on whether any lessons learned should be saved.
405
- - Call `memory_reflect` to automatically distribute observations into slots if needed.
406
  """
407
  cwd = os.getcwd()
408
 
@@ -435,7 +435,7 @@ ADAPTERS = [
435
  ]
436
 
437
  def main():
438
- parser = argparse.ArgumentParser(description="Wired agentmemory MCP and Hooks into client agents.")
439
  parser.add_argument("agent", nargs="?", choices=[a.name for a in ADAPTERS], help="Specify target agent.")
440
  parser.add_argument("--with-hooks", action="store_true", help="Install global workspace hook execution blocks (Claude/Codex).")
441
  parser.add_argument("--dry-run", action="store_true", help="Log proposed configuration modifications without writing.")
 
124
  next_cfg = existing.copy()
125
  servers = next_cfg.get("mcpServers", {})
126
 
127
+ already_has = "agentcache" in servers
128
  if already_has and not args.force:
129
  print(f"[OK] Claude Code already wired in {claude_json}")
130
  else:
131
  if args.dry_run:
132
+ print(f"[dry-run] Would write mcpServers.agentcache in {claude_json}")
133
  else:
134
  backup = backup_file(claude_json, "claude-code")
135
  if backup:
136
  print(f"Backed up configuration to {backup}")
137
 
138
+ env = {"AGENTCACHE_URL": os.environ.get("AGENTCACHE_URL") or os.environ.get("AGENTMEMORY_URL") or "http://localhost:3111"}
139
+ secret = os.environ.get("AGENTCACHE_SECRET") or os.environ.get("AGENTMEMORY_SECRET")
140
  if secret:
141
+ env["AGENTCACHE_SECRET"] = secret
142
+ servers["agentcache"] = {
143
  "command": sys.executable,
144
  "args": [mcp_stdio_path],
145
  "env": env
 
179
  codex_toml = os.path.join(get_home_dir(), ".codex", "config.toml")
180
  mcp_stdio_path = get_mcp_stdio_path()
181
 
182
+ url = os.environ.get("AGENTCACHE_URL") or os.environ.get("AGENTMEMORY_URL") or "http://localhost:3111"
183
+ secret = os.environ.get("AGENTCACHE_SECRET") or os.environ.get("AGENTMEMORY_SECRET")
184
  toml_block = f"""
185
+ [mcp_servers.agentcache]
186
  command = "{sys.executable.replace('\\', '/')}"
187
  args = ["{mcp_stdio_path.replace('\\', '/')}"]
188
+ [mcp_servers.agentcache.env]
189
+ AGENTCACHE_URL = "{url}"
190
  """
191
  if secret:
192
+ toml_block += f'AGENTCACHE_SECRET = "{secret}"\n'
193
 
194
  exists = os.path.exists(codex_toml)
195
  current = ""
 
197
  with open(codex_toml, "r", encoding="utf-8") as f:
198
  current = f.read()
199
 
200
+ wired = "[mcp_servers.agentcache]" in current
201
  if wired and not args.force:
202
  print(f"[OK] Codex CLI already wired in {codex_toml}")
203
  else:
204
  if args.dry_run:
205
+ print(f"[dry-run] Would write [mcp_servers.agentcache] block to {codex_toml}")
206
  else:
207
  backup = backup_file(codex_toml, "codex", "toml")
208
  if backup:
 
216
  skipping = False
217
  for line in lines:
218
  trimmed = line.strip()
219
+ if trimmed == "[mcp_servers.agentcache]" or trimmed == "[mcp_servers.agentcache.env]":
220
  skipping = True
221
  continue
222
+ if skipping and trimmed.startswith("[") and trimmed != "[mcp_servers.agentcache.env]":
223
  skipping = False
224
  if not skipping:
225
  out.append(line)
 
258
  return os.path.exists(hermes_dir)
259
 
260
  def install(self, args):
261
+ dest_dir = os.path.join(get_home_dir(), ".hermes", "plugins", "agentcache")
262
  src_dir = os.path.dirname(os.path.abspath(__file__))
263
  project_root = os.path.dirname(src_dir)
264
  hermes_src = os.path.join(project_root, "integrations", "hermes")
 
277
  shutil.rmtree(dest_dir)
278
 
279
  shutil.copytree(hermes_src, dest_dir)
280
+ print(f"[OK] Copied Hermes cache provider plugin to {dest_dir}")
281
  print("To finish configuration, add to ~/.hermes/config.yaml:")
282
  print(" mcp_servers:")
283
+ print(" agentcache:")
284
  print(" command: python")
285
  print(f' args: ["{get_mcp_stdio_path()}"]')
286
+ print(" cache:")
287
+ print(" provider: agentcache")
288
 
289
  class AntigravityAdapter:
290
  name = "antigravity"
 
310
  next_cfg = existing.copy()
311
  servers = next_cfg.get("mcpServers", {})
312
 
313
+ already_has = "agentcache" in servers
314
  if already_has and not args.force:
315
  print(f"[OK] Antigravity already wired in {mcp_config_path}")
316
  else:
317
  if args.dry_run:
318
+ print(f"[dry-run] Would write mcpServers.agentcache in {mcp_config_path}")
319
  else:
320
  backup = backup_file(mcp_config_path, "antigravity")
321
  if backup:
322
  print(f"Backed up config to {backup}")
323
 
324
+ env = {"AGENTCACHE_URL": os.environ.get("AGENTCACHE_URL") or os.environ.get("AGENTMEMORY_URL") or "http://localhost:3111"}
325
+ secret = os.environ.get("AGENTCACHE_SECRET") or os.environ.get("AGENTMEMORY_SECRET")
326
  if secret:
327
+ env["AGENTCACHE_SECRET"] = secret
328
+ servers["agentcache"] = {
329
  "command": sys.executable,
330
  "args": [mcp_stdio_path],
331
  "env": env
 
350
  next_cfg = existing.copy()
351
  servers = next_cfg.get("mcpServers", {})
352
 
353
+ already_has = "agentcache" in servers
354
  if already_has and not args.force:
355
  print(f"[OK] Kiro already wired in {mcp_config_path}")
356
  else:
357
  if args.dry_run:
358
+ print(f"[dry-run] Would write mcpServers.agentcache in {mcp_config_path}")
359
  else:
360
  backup = backup_file(mcp_config_path, "kiro")
361
  if backup:
362
  print(f"Backed up config to {backup}")
363
 
364
+ env = {"AGENTCACHE_URL": os.environ.get("AGENTCACHE_URL") or os.environ.get("AGENTMEMORY_URL") or "http://localhost:3111"}
365
+ secret = os.environ.get("AGENTCACHE_SECRET") or os.environ.get("AGENTMEMORY_SECRET")
366
  if secret:
367
+ env["AGENTCACHE_SECRET"] = secret
368
+ servers["agentcache"] = {
369
  "command": sys.executable,
370
  "args": [mcp_stdio_path],
371
  "env": env
 
383
  return True
384
 
385
  def install(self, args):
386
+ rule_content = """# Agent Cache Rules
387
 
388
+ This workspace is integrated with long-term semantic memory via `agentcache-python`.
389
+ You must act as your own cache manager by calling the cache MCP tools at critical boundaries.
390
 
391
  ## Rules & Workflow
392
 
393
  1. **Initial Search (Prefetch Context)**:
394
+ At the start of every session or new task, immediately call `cache_smart_search` with terms related to the current objective. This retrieves past architecture patterns, preferences, bug fixes, or lessons.
395
+ - Example: `cache_smart_search(query="jwt token rotation logic")`
396
 
397
  2. **Lessons & Insights Capture**:
398
  When you successfully debug a complex error, discover an undocumented requirement, or establish a convention, persist it:
399
+ - Call `cache_lesson_save` to record lessons that improve your coding capabilities. Duplicate saves strengthen confidence scores.
400
+ - Call `cache_save` to save long-term structural facts. Always extract 2-5 specific lowercased tags (e.g. `auth-flow`, `refresh-token`) as concepts.
401
 
402
  3. **Checklist Before Ending**:
403
  Before stating a task is complete:
404
  - Reflect on whether any lessons learned should be saved.
405
+ - Call `cache_reflect` to automatically distribute observations into slots if needed.
406
  """
407
  cwd = os.getcwd()
408
 
 
435
  ]
436
 
437
  def main():
438
+ parser = argparse.ArgumentParser(description="Wired agentcache MCP and Hooks into client agents.")
439
  parser.add_argument("agent", nargs="?", choices=[a.name for a in ADAPTERS], help="Specify target agent.")
440
  parser.add_argument("--with-hooks", action="store_true", help="Install global workspace hook execution blocks (Claude/Codex).")
441
  parser.add_argument("--dry-run", action="store_true", help="Log proposed configuration modifications without writing.")
src/db.py CHANGED
@@ -8,7 +8,7 @@ from typing import Dict, Any, List, Optional, TypeVar, Union
8
 
9
  T = TypeVar("T")
10
 
11
- DB_PATH = os.path.join(os.path.expanduser("~"), ".agentmemory", "agentmemory.db")
12
 
13
 
14
  class StateKV:
 
8
 
9
  T = TypeVar("T")
10
 
11
+ DB_PATH = os.path.join(os.path.expanduser("~"), ".agentcache", "agentcache.db")
12
 
13
 
14
  class StateKV:
src/functions.py CHANGED
@@ -409,10 +409,10 @@ def query_audit(
409
  # Image Store System
410
  # =====================================================================
411
 
412
- IMAGES_DIR = os.path.join(os.path.expanduser("~"), ".agentmemory", "images")
413
 
414
  def get_max_bytes() -> int:
415
- return int(os.getenv("AGENTMEMORY_IMAGE_STORE_MAX_BYTES", 500 * 1024 * 1024))
416
 
417
  def is_managed_image_path(file_path: str) -> bool:
418
  if not file_path:
@@ -468,7 +468,7 @@ def delete_image(file_path: Optional[str]) -> int:
468
  os.remove(file_path)
469
  return size
470
  except Exception as e:
471
- print(f"[agentmemory] Failed to delete image context: {e}")
472
  return 0
473
 
474
  def touch_image(file_path: str) -> None:
@@ -680,16 +680,16 @@ def commit_if_enabled(kv: StateKV, message: str, agent_id: Optional[str]) -> Opt
680
 
681
 
682
  def is_agent_scope_isolated() -> bool:
683
- return os.getenv("AGENTMEMORY_AGENT_SCOPE") == "isolated"
684
 
685
  def is_auto_compress_enabled() -> bool:
686
- return os.getenv("AGENTMEMORY_AUTO_COMPRESS") == "true"
687
 
688
  def is_slots_enabled() -> bool:
689
- return os.getenv("AGENTMEMORY_SLOTS") == "true"
690
 
691
  def is_reflect_enabled() -> bool:
692
- return os.getenv("AGENTMEMORY_REFLECT") == "true"
693
 
694
  def is_graph_extraction_enabled() -> bool:
695
  return os.getenv("GRAPH_EXTRACTION_ENABLED") == "true"
@@ -1825,8 +1825,8 @@ def context(kv: StateKV, data: Dict[str, Any]) -> Dict[str, Any]:
1825
 
1826
  blocks.sort(key=lambda b: b.get("recency", 0), reverse=True)
1827
 
1828
- header = f'<agentmemory-context project="{escape_xml_attr(project)}">'
1829
- footer = "</agentmemory-context>"
1830
  used_tokens = estimate_tokens(header) + estimate_tokens(footer)
1831
 
1832
  selected = []
@@ -1949,7 +1949,7 @@ def list_pinned_slots(kv: StateKV, project: Optional[str] = None) -> List[Dict[s
1949
  def render_pinned_context(slots: List[Dict[str, Any]]) -> str:
1950
  if not slots:
1951
  return ""
1952
- lines = ["# agentmemory pinned slots", ""]
1953
  for s in slots:
1954
  lines.append(f"## {s['label']}")
1955
  lines.append(s["content"].strip())
@@ -3006,7 +3006,7 @@ def health_check(kv: StateKV) -> Dict[str, Any]:
3006
  db_size_bytes = 0
3007
  wal_size_bytes = 0
3008
  try:
3009
- sync_state_path = os.path.join(os.path.expanduser("~"), ".agentmemory", ".sync_state")
3010
  if os.path.exists(sync_state_path):
3011
  with open(sync_state_path, "r", encoding="utf-8") as _sf:
3012
  _sync = json.loads(_sf.read())
 
409
  # Image Store System
410
  # =====================================================================
411
 
412
+ IMAGES_DIR = os.path.join(os.path.expanduser("~"), ".agentcache", "images")
413
 
414
  def get_max_bytes() -> int:
415
+ return int(os.getenv("AGENTCACHE_IMAGE_STORE_MAX_BYTES") or os.getenv("AGENTMEMORY_IMAGE_STORE_MAX_BYTES") or 500 * 1024 * 1024)
416
 
417
  def is_managed_image_path(file_path: str) -> bool:
418
  if not file_path:
 
468
  os.remove(file_path)
469
  return size
470
  except Exception as e:
471
+ print(f"[agentcache] Failed to delete image context: {e}")
472
  return 0
473
 
474
  def touch_image(file_path: str) -> None:
 
680
 
681
 
682
  def is_agent_scope_isolated() -> bool:
683
+ return (os.getenv("AGENTCACHE_AGENT_SCOPE") or os.getenv("AGENTMEMORY_AGENT_SCOPE")) == "isolated"
684
 
685
  def is_auto_compress_enabled() -> bool:
686
+ return (os.getenv("AGENTCACHE_AUTO_COMPRESS") or os.getenv("AGENTMEMORY_AUTO_COMPRESS")) == "true"
687
 
688
  def is_slots_enabled() -> bool:
689
+ return (os.getenv("AGENTCACHE_SLOTS") or os.getenv("AGENTMEMORY_SLOTS")) == "true"
690
 
691
  def is_reflect_enabled() -> bool:
692
+ return (os.getenv("AGENTCACHE_REFLECT") or os.getenv("AGENTMEMORY_REFLECT")) == "true"
693
 
694
  def is_graph_extraction_enabled() -> bool:
695
  return os.getenv("GRAPH_EXTRACTION_ENABLED") == "true"
 
1825
 
1826
  blocks.sort(key=lambda b: b.get("recency", 0), reverse=True)
1827
 
1828
+ header = f'<agentcache-context project="{escape_xml_attr(project)}">'
1829
+ footer = "</agentcache-context>"
1830
  used_tokens = estimate_tokens(header) + estimate_tokens(footer)
1831
 
1832
  selected = []
 
1949
  def render_pinned_context(slots: List[Dict[str, Any]]) -> str:
1950
  if not slots:
1951
  return ""
1952
+ lines = ["# agentcache pinned slots", ""]
1953
  for s in slots:
1954
  lines.append(f"## {s['label']}")
1955
  lines.append(s["content"].strip())
 
3006
  db_size_bytes = 0
3007
  wal_size_bytes = 0
3008
  try:
3009
+ sync_state_path = os.path.join(os.path.expanduser("~"), ".agentcache", ".sync_state")
3010
  if os.path.exists(sync_state_path):
3011
  with open(sync_state_path, "r", encoding="utf-8") as _sf:
3012
  _sync = json.loads(_sf.read())
src/import_data.py CHANGED
@@ -53,7 +53,7 @@ def import_old_data(old_db_path: str, kv: StateKV) -> bool:
53
 
54
  if count > 0:
55
  print("[import] Creating Dolt version commit...")
56
- kv.commit_version("Import legacy AgentMemory database from Hugging Face", "system")
57
 
58
  print(f"[import] Finished! Migrated {count} total records into Dolt SQL.")
59
  return True
@@ -61,8 +61,8 @@ def import_old_data(old_db_path: str, kv: StateKV) -> bool:
61
 
62
  if __name__ == "__main__":
63
  # Prioritize the restored HF data in the user's home folder, falling back to local workspace data
64
- home_path = os.path.expanduser(os.path.join("~", ".agentmemory", "state_store.db"))
65
- workspace_path = r"d:\Downloads\Projects\Other Projects\agentmemory\agentmemory\data\state_store.db"
66
 
67
  if os.path.exists(home_path):
68
  default_path = home_path
 
53
 
54
  if count > 0:
55
  print("[import] Creating Dolt version commit...")
56
+ kv.commit_version("Import legacy AgentCache database from Hugging Face", "system")
57
 
58
  print(f"[import] Finished! Migrated {count} total records into Dolt SQL.")
59
  return True
 
61
 
62
  if __name__ == "__main__":
63
  # Prioritize the restored HF data in the user's home folder, falling back to local workspace data
64
+ home_path = os.path.expanduser(os.path.join("~", ".agentcache", "state_store.db"))
65
+ workspace_path = r"d:\Downloads\Projects\Other Projects\agentcache\agentcache\data\state_store.db"
66
 
67
  if os.path.exists(home_path):
68
  default_path = home_path
src/mcp_stdio.py CHANGED
@@ -13,11 +13,11 @@ import json
13
  import os
14
  import requests
15
 
16
- BASE = os.getenv("AGENTMEMORY_URL", "http://127.0.0.1:3111").rstrip("/")
17
- if not BASE.endswith("/agentmemory"):
18
- BASE = f"{BASE}/agentmemory"
19
 
20
- _secret = os.getenv("AGENTMEMORY_SECRET")
21
 
22
 
23
  def headers():
@@ -44,7 +44,7 @@ def handle(req):
44
  "result": {
45
  "protocolVersion": "2024-11-05",
46
  "capabilities": {"tools": {}},
47
- "serverInfo": {"name": "agentmemory-local", "version": "0.9.8"}
48
  }
49
  })
50
 
 
13
  import os
14
  import requests
15
 
16
+ BASE = (os.getenv("AGENTCACHE_URL") or os.getenv("AGENTMEMORY_URL") or "http://127.0.0.1:3111").rstrip("/")
17
+ if not BASE.endswith("/agentcache") and not BASE.endswith("/agentmemory"):
18
+ BASE = f"{BASE}/agentcache"
19
 
20
+ _secret = os.getenv("AGENTCACHE_SECRET") or os.getenv("AGENTMEMORY_SECRET")
21
 
22
 
23
  def headers():
 
44
  "result": {
45
  "protocolVersion": "2024-11-05",
46
  "capabilities": {"tools": {}},
47
+ "serverInfo": {"name": "agentcache-local", "version": "0.9.8"}
48
  }
49
  })
50
 
src/routes/graph.py CHANGED
@@ -18,7 +18,7 @@ graph_bp = Blueprint("graph", __name__)
18
 
19
  def _check_auth():
20
  import hmac
21
- secret = os.getenv("AGENTMEMORY_SECRET")
22
  if not secret:
23
  return None
24
  auth = request.headers.get("Authorization") or request.headers.get("authorization")
@@ -36,9 +36,10 @@ def _get_kv():
36
 
37
 
38
  # ---------------------------------------------------------------------------
39
- # GET /agentmemory/graph
40
  # ---------------------------------------------------------------------------
41
 
 
42
  @graph_bp.route("/agentmemory/graph", methods=["GET"])
43
  def api_graph():
44
  auth_err = _check_auth()
@@ -49,9 +50,10 @@ def api_graph():
49
 
50
 
51
  # ---------------------------------------------------------------------------
52
- # GET /agentmemory/graph/stats
53
  # ---------------------------------------------------------------------------
54
 
 
55
  @graph_bp.route("/agentmemory/graph/stats", methods=["GET"])
56
  def api_graph_stats():
57
  auth_err = _check_auth()
@@ -67,9 +69,10 @@ def api_graph_stats():
67
 
68
 
69
  # ---------------------------------------------------------------------------
70
- # POST /agentmemory/graph/query
71
  # ---------------------------------------------------------------------------
72
 
 
73
  @graph_bp.route("/agentmemory/graph/query", methods=["POST"])
74
  def api_graph_query():
75
  auth_err = _check_auth()
@@ -84,9 +87,10 @@ def api_graph_query():
84
 
85
 
86
  # ---------------------------------------------------------------------------
87
- # POST /agentmemory/graph/build
88
  # ---------------------------------------------------------------------------
89
 
 
90
  @graph_bp.route("/agentmemory/graph/build", methods=["POST"])
91
  def api_graph_build():
92
  auth_err = _check_auth()
 
18
 
19
  def _check_auth():
20
  import hmac
21
+ secret = os.getenv("AGENTCACHE_SECRET") or os.getenv("AGENTMEMORY_SECRET")
22
  if not secret:
23
  return None
24
  auth = request.headers.get("Authorization") or request.headers.get("authorization")
 
36
 
37
 
38
  # ---------------------------------------------------------------------------
39
+ # GET /agentcache/graph
40
  # ---------------------------------------------------------------------------
41
 
42
+ @graph_bp.route("/agentcache/graph", methods=["GET"])
43
  @graph_bp.route("/agentmemory/graph", methods=["GET"])
44
  def api_graph():
45
  auth_err = _check_auth()
 
50
 
51
 
52
  # ---------------------------------------------------------------------------
53
+ # GET /agentcache/graph/stats
54
  # ---------------------------------------------------------------------------
55
 
56
+ @graph_bp.route("/agentcache/graph/stats", methods=["GET"])
57
  @graph_bp.route("/agentmemory/graph/stats", methods=["GET"])
58
  def api_graph_stats():
59
  auth_err = _check_auth()
 
69
 
70
 
71
  # ---------------------------------------------------------------------------
72
+ # POST /agentcache/graph/query
73
  # ---------------------------------------------------------------------------
74
 
75
+ @graph_bp.route("/agentcache/graph/query", methods=["POST"])
76
  @graph_bp.route("/agentmemory/graph/query", methods=["POST"])
77
  def api_graph_query():
78
  auth_err = _check_auth()
 
87
 
88
 
89
  # ---------------------------------------------------------------------------
90
+ # POST /agentcache/graph/build
91
  # ---------------------------------------------------------------------------
92
 
93
+ @graph_bp.route("/agentcache/graph/build", methods=["POST"])
94
  @graph_bp.route("/agentmemory/graph/build", methods=["POST"])
95
  def api_graph_build():
96
  auth_err = _check_auth()
src/routes/health.py CHANGED
@@ -18,7 +18,7 @@ health_bp = Blueprint("health", __name__)
18
 
19
  def _check_auth():
20
  import hmac
21
- secret = os.getenv("AGENTMEMORY_SECRET")
22
  if not secret:
23
  return None
24
  auth = request.headers.get("Authorization") or request.headers.get("authorization")
@@ -41,33 +41,36 @@ def _get_embedding_provider():
41
 
42
 
43
  # ---------------------------------------------------------------------------
44
- # GET /agentmemory/livez (no auth required)
45
  # ---------------------------------------------------------------------------
46
 
 
47
  @health_bp.route("/agentmemory/livez", methods=["GET"])
48
  def livez():
49
  port = int(os.getenv("III_REST_PORT", os.getenv("PORT", "3111")))
50
  return jsonify({
51
  "status": "ok",
52
- "service": "agentmemory",
53
  "viewerPort": port,
54
  "viewerSkipped": False,
55
  })
56
 
57
 
58
  # ---------------------------------------------------------------------------
59
- # GET /agentmemory/health
60
  # ---------------------------------------------------------------------------
61
 
 
62
  @health_bp.route("/agentmemory/health", methods=["GET"])
63
  def health():
64
  return jsonify(functions.health_check(_get_kv()))
65
 
66
 
67
  # ---------------------------------------------------------------------------
68
- # GET /agentmemory/audit
69
  # ---------------------------------------------------------------------------
70
 
 
71
  @health_bp.route("/agentmemory/audit", methods=["GET"])
72
  def api_audit():
73
  auth_err = _check_auth()
@@ -81,9 +84,10 @@ def api_audit():
81
 
82
 
83
  # ---------------------------------------------------------------------------
84
- # GET /agentmemory/config/flags
85
  # ---------------------------------------------------------------------------
86
 
 
87
  @health_bp.route("/agentmemory/config/flags", methods=["GET"])
88
  def config_flags():
89
  auth_err = _check_auth()
@@ -118,14 +122,14 @@ def config_flags():
118
  "docsHref": "https://github.com/rohitg00/agentmemory#consolidation",
119
  },
120
  {
121
- "key": "AGENTMEMORY_AUTO_COMPRESS",
122
  "label": "LLM-powered observation compression",
123
  "enabled": functions.is_auto_compress_enabled(),
124
  "default": False,
125
  "affects": ["Memories", "Timeline"],
126
  "needsLlm": True,
127
  "description": "Every observation is compressed by the LLM for richer summaries. OFF uses synthetic compression.",
128
- "enableHow": "Set AGENTMEMORY_AUTO_COMPRESS=true.",
129
  "docsHref": "https://github.com/rohitg00/agentmemory/issues/138",
130
  },
131
  ]
 
18
 
19
  def _check_auth():
20
  import hmac
21
+ secret = os.getenv("AGENTCACHE_SECRET") or os.getenv("AGENTMEMORY_SECRET")
22
  if not secret:
23
  return None
24
  auth = request.headers.get("Authorization") or request.headers.get("authorization")
 
41
 
42
 
43
  # ---------------------------------------------------------------------------
44
+ # GET /agentcache/livez (no auth required)
45
  # ---------------------------------------------------------------------------
46
 
47
+ @health_bp.route("/agentcache/livez", methods=["GET"])
48
  @health_bp.route("/agentmemory/livez", methods=["GET"])
49
  def livez():
50
  port = int(os.getenv("III_REST_PORT", os.getenv("PORT", "3111")))
51
  return jsonify({
52
  "status": "ok",
53
+ "service": "agentcache",
54
  "viewerPort": port,
55
  "viewerSkipped": False,
56
  })
57
 
58
 
59
  # ---------------------------------------------------------------------------
60
+ # GET /agentcache/health
61
  # ---------------------------------------------------------------------------
62
 
63
+ @health_bp.route("/agentcache/health", methods=["GET"])
64
  @health_bp.route("/agentmemory/health", methods=["GET"])
65
  def health():
66
  return jsonify(functions.health_check(_get_kv()))
67
 
68
 
69
  # ---------------------------------------------------------------------------
70
+ # GET /agentcache/audit
71
  # ---------------------------------------------------------------------------
72
 
73
+ @health_bp.route("/agentcache/audit", methods=["GET"])
74
  @health_bp.route("/agentmemory/audit", methods=["GET"])
75
  def api_audit():
76
  auth_err = _check_auth()
 
84
 
85
 
86
  # ---------------------------------------------------------------------------
87
+ # GET /agentcache/config/flags
88
  # ---------------------------------------------------------------------------
89
 
90
+ @health_bp.route("/agentcache/config/flags", methods=["GET"])
91
  @health_bp.route("/agentmemory/config/flags", methods=["GET"])
92
  def config_flags():
93
  auth_err = _check_auth()
 
122
  "docsHref": "https://github.com/rohitg00/agentmemory#consolidation",
123
  },
124
  {
125
+ "key": "AGENTCACHE_AUTO_COMPRESS",
126
  "label": "LLM-powered observation compression",
127
  "enabled": functions.is_auto_compress_enabled(),
128
  "default": False,
129
  "affects": ["Memories", "Timeline"],
130
  "needsLlm": True,
131
  "description": "Every observation is compressed by the LLM for richer summaries. OFF uses synthetic compression.",
132
+ "enableHow": "Set AGENTCACHE_AUTO_COMPRESS=true.",
133
  "docsHref": "https://github.com/rohitg00/agentmemory/issues/138",
134
  },
135
  ]
src/routes/mcp.py CHANGED
@@ -18,7 +18,7 @@ mcp_bp = Blueprint("mcp", __name__)
18
 
19
  def _check_auth():
20
  import hmac
21
- secret = os.getenv("AGENTMEMORY_SECRET")
22
  if not secret:
23
  return None
24
  auth = request.headers.get("Authorization") or request.headers.get("authorization")
@@ -49,9 +49,10 @@ def _parse_mcp_list_arg(arg_val):
49
 
50
 
51
  # ---------------------------------------------------------------------------
52
- # GET /agentmemory/mcp/tools
53
  # ---------------------------------------------------------------------------
54
 
 
55
  @mcp_bp.route("/agentmemory/mcp/tools", methods=["GET"])
56
  def mcp_tools_list():
57
  auth_err = _check_auth()
@@ -60,7 +61,7 @@ def mcp_tools_list():
60
 
61
  tools = [
62
  {
63
- "name": "memory_recall",
64
  "description": "Search past folder observations and global memories. Use when you need to recall what happened in a folder.",
65
  "inputSchema": {
66
  "type": "object",
@@ -74,7 +75,7 @@ def mcp_tools_list():
74
  },
75
  },
76
  {
77
- "name": "memory_smart_search",
78
  "description": "Hybrid semantic+keyword search across folder observations and global memories.",
79
  "inputSchema": {
80
  "type": "object",
@@ -88,7 +89,7 @@ def mcp_tools_list():
88
  },
89
  },
90
  {
91
- "name": "memory_save",
92
  "description": "Explicitly save an important insight, decision, or pattern to long-term memory.",
93
  "inputSchema": {
94
  "type": "object",
@@ -113,12 +114,12 @@ def mcp_tools_list():
113
  },
114
  },
115
  {
116
- "name": "memory_diagnose",
117
  "description": "Health check — returns folder, agent, observation and memory counts.",
118
  "inputSchema": {"type": "object", "properties": {}},
119
  },
120
  {
121
- "name": "memory_forget",
122
  "description": "Delete a global memory or all observations for a (folderPath, agentId) pair.",
123
  "inputSchema": {
124
  "type": "object",
@@ -136,7 +137,7 @@ def mcp_tools_list():
136
  },
137
  },
138
  {
139
- "name": "memory_export",
140
  "description": "Export all folder observations and global memories as JSON (v2 format).",
141
  "inputSchema": {"type": "object", "properties": {}},
142
  },
@@ -161,7 +162,7 @@ def mcp_tools_list():
161
  },
162
  },
163
  {
164
- "name": "agent_remember",
165
  "description": "Explicitly save a key insight, fact, or architecture decision to long-term memory.",
166
  "inputSchema": {
167
  "type": "object",
@@ -187,12 +188,12 @@ def mcp_tools_list():
187
  },
188
  },
189
  {
190
- "name": "memory_folders",
191
  "description": "List all (folder, agent) pairs that have memory observations.",
192
  "inputSchema": {"type": "object", "properties": {}},
193
  },
194
  {
195
- "name": "memory_folder_observations",
196
  "description": "Get all observations for a specific (folderPath, agentId) pair.",
197
  "inputSchema": {
198
  "type": "object",
@@ -204,7 +205,7 @@ def mcp_tools_list():
204
  },
205
  },
206
  {
207
- "name": "memory_timeline",
208
  "description": "Get folder activity feed — observations sorted by time, filterable by folder/agent.",
209
  "inputSchema": {
210
  "type": "object",
@@ -218,7 +219,7 @@ def mcp_tools_list():
218
  },
219
  },
220
  {
221
- "name": "memory_dedup",
222
  "description": "Remove duplicate observations from a (folderPath, agentId) pair or all pairs. Keeps the earliest observation per unique text fingerprint.",
223
  "inputSchema": {
224
  "type": "object",
@@ -233,9 +234,10 @@ def mcp_tools_list():
233
 
234
 
235
  # ---------------------------------------------------------------------------
236
- # POST /agentmemory/mcp/tools
237
  # ---------------------------------------------------------------------------
238
 
 
239
  @mcp_bp.route("/agentmemory/mcp/tools", methods=["POST"])
240
  def mcp_tools_call():
241
  auth_err = _check_auth()
@@ -253,7 +255,7 @@ def mcp_tools_call():
253
  print(f"[mcp] Calling tool {name} with args: {args}")
254
  text_out = ""
255
 
256
- if name == "memory_recall":
257
  q = args.get("query")
258
  limit = int(args.get("limit") or 10)
259
  folder_path = args.get("folderPath")
@@ -267,7 +269,7 @@ def mcp_tools_call():
267
  res = functions.folder_search(kv, q, limit, folder_path=folder_path, agent_id=agent_id)
268
  text_out = json.dumps(res, indent=2)
269
 
270
- elif name == "memory_save":
271
  content = args.get("content")
272
  concepts = _parse_mcp_list_arg(args.get("concepts"))
273
  files = _parse_mcp_list_arg(args.get("files"))
@@ -281,7 +283,7 @@ def mcp_tools_call():
281
  })
282
  text_out = json.dumps(res)
283
 
284
- elif name == "memory_smart_search":
285
  q = args.get("query")
286
  limit = int(args.get("limit") or 10)
287
  folder_path = args.get("folderPath")
@@ -295,11 +297,11 @@ def mcp_tools_call():
295
  res = functions.folder_search(kv, q, limit, folder_path=folder_path, agent_id=agent_id)
296
  text_out = json.dumps(res, indent=2)
297
 
298
- elif name == "memory_diagnose":
299
  res = functions.health_check(kv)
300
  text_out = json.dumps(res, indent=2)
301
 
302
- elif name == "memory_forget":
303
  obs_ids = _parse_mcp_list_arg(args.get("observationIds"))
304
  res = functions.forget(kv, {
305
  "memoryId": args.get("memoryId"),
@@ -309,7 +311,7 @@ def mcp_tools_call():
309
  })
310
  text_out = json.dumps(res, indent=2)
311
 
312
- elif name == "memory_export":
313
  res = functions.export_data(kv, {})
314
  text_out = json.dumps(res, indent=2)
315
 
@@ -325,6 +327,7 @@ def mcp_tools_call():
325
  folder_path = (
326
  args.get("cwd")
327
  or args.get("project")
 
328
  or os.getenv("AGENTMEMORY_CWD")
329
  or "/unknown"
330
  )
@@ -361,7 +364,7 @@ def mcp_tools_call():
361
  res = functions.folder_observe(kv, payload)
362
  text_out = json.dumps(res)
363
 
364
- elif name == "agent_remember":
365
  agent_id = args.get("agentId") or functions.get_agent_id() or "agent"
366
  content = args.get("content")
367
  project = args.get("project")
@@ -389,7 +392,7 @@ def mcp_tools_call():
389
  res = functions.remember(kv, payload)
390
  text_out = json.dumps(res)
391
 
392
- elif name == "memory_folders":
393
  pairs = sorted(
394
  kv.list(KV.folders),
395
  key=lambda x: x.get("lastUpdated", ""),
@@ -401,7 +404,7 @@ def mcp_tools_call():
401
  pairs = [p for p in pairs if p.get("agentId") == aid]
402
  text_out = json.dumps(pairs, indent=2)
403
 
404
- elif name == "memory_folder_observations":
405
  fp = args.get("folderPath", "")
406
  aid = args.get("agentId", "")
407
  if not fp or not aid:
@@ -417,7 +420,7 @@ def mcp_tools_call():
417
  )
418
  text_out = json.dumps(obs, indent=2)
419
 
420
- elif name == "memory_timeline":
421
  request_aid = args.get("agentId")
422
  if functions.is_agent_scope_isolated():
423
  current_aid = functions.get_agent_id()
@@ -435,7 +438,7 @@ def mcp_tools_call():
435
  )
436
  text_out = json.dumps(res, indent=2)
437
 
438
- elif name == "memory_dedup":
439
  res = functions.dedup_folder_observations(
440
  kv,
441
  args.get("folderPath") or None,
 
18
 
19
  def _check_auth():
20
  import hmac
21
+ secret = os.getenv("AGENTCACHE_SECRET") or os.getenv("AGENTMEMORY_SECRET")
22
  if not secret:
23
  return None
24
  auth = request.headers.get("Authorization") or request.headers.get("authorization")
 
49
 
50
 
51
  # ---------------------------------------------------------------------------
52
+ # GET /agentcache/mcp/tools
53
  # ---------------------------------------------------------------------------
54
 
55
+ @mcp_bp.route("/agentcache/mcp/tools", methods=["GET"])
56
  @mcp_bp.route("/agentmemory/mcp/tools", methods=["GET"])
57
  def mcp_tools_list():
58
  auth_err = _check_auth()
 
61
 
62
  tools = [
63
  {
64
+ "name": "cache_recall",
65
  "description": "Search past folder observations and global memories. Use when you need to recall what happened in a folder.",
66
  "inputSchema": {
67
  "type": "object",
 
75
  },
76
  },
77
  {
78
+ "name": "cache_smart_search",
79
  "description": "Hybrid semantic+keyword search across folder observations and global memories.",
80
  "inputSchema": {
81
  "type": "object",
 
89
  },
90
  },
91
  {
92
+ "name": "cache_save",
93
  "description": "Explicitly save an important insight, decision, or pattern to long-term memory.",
94
  "inputSchema": {
95
  "type": "object",
 
114
  },
115
  },
116
  {
117
+ "name": "cache_diagnose",
118
  "description": "Health check — returns folder, agent, observation and memory counts.",
119
  "inputSchema": {"type": "object", "properties": {}},
120
  },
121
  {
122
+ "name": "cache_forget",
123
  "description": "Delete a global memory or all observations for a (folderPath, agentId) pair.",
124
  "inputSchema": {
125
  "type": "object",
 
137
  },
138
  },
139
  {
140
+ "name": "cache_export",
141
  "description": "Export all folder observations and global memories as JSON (v2 format).",
142
  "inputSchema": {"type": "object", "properties": {}},
143
  },
 
162
  },
163
  },
164
  {
165
+ "name": "agent_cache",
166
  "description": "Explicitly save a key insight, fact, or architecture decision to long-term memory.",
167
  "inputSchema": {
168
  "type": "object",
 
188
  },
189
  },
190
  {
191
+ "name": "cache_folders",
192
  "description": "List all (folder, agent) pairs that have memory observations.",
193
  "inputSchema": {"type": "object", "properties": {}},
194
  },
195
  {
196
+ "name": "cache_folder_observations",
197
  "description": "Get all observations for a specific (folderPath, agentId) pair.",
198
  "inputSchema": {
199
  "type": "object",
 
205
  },
206
  },
207
  {
208
+ "name": "cache_timeline",
209
  "description": "Get folder activity feed — observations sorted by time, filterable by folder/agent.",
210
  "inputSchema": {
211
  "type": "object",
 
219
  },
220
  },
221
  {
222
+ "name": "cache_dedup",
223
  "description": "Remove duplicate observations from a (folderPath, agentId) pair or all pairs. Keeps the earliest observation per unique text fingerprint.",
224
  "inputSchema": {
225
  "type": "object",
 
234
 
235
 
236
  # ---------------------------------------------------------------------------
237
+ # POST /agentcache/mcp/tools
238
  # ---------------------------------------------------------------------------
239
 
240
+ @mcp_bp.route("/agentcache/mcp/tools", methods=["POST"])
241
  @mcp_bp.route("/agentmemory/mcp/tools", methods=["POST"])
242
  def mcp_tools_call():
243
  auth_err = _check_auth()
 
255
  print(f"[mcp] Calling tool {name} with args: {args}")
256
  text_out = ""
257
 
258
+ if name in ("cache_recall", "memory_recall"):
259
  q = args.get("query")
260
  limit = int(args.get("limit") or 10)
261
  folder_path = args.get("folderPath")
 
269
  res = functions.folder_search(kv, q, limit, folder_path=folder_path, agent_id=agent_id)
270
  text_out = json.dumps(res, indent=2)
271
 
272
+ elif name in ("cache_save", "memory_save"):
273
  content = args.get("content")
274
  concepts = _parse_mcp_list_arg(args.get("concepts"))
275
  files = _parse_mcp_list_arg(args.get("files"))
 
283
  })
284
  text_out = json.dumps(res)
285
 
286
+ elif name in ("cache_smart_search", "memory_smart_search"):
287
  q = args.get("query")
288
  limit = int(args.get("limit") or 10)
289
  folder_path = args.get("folderPath")
 
297
  res = functions.folder_search(kv, q, limit, folder_path=folder_path, agent_id=agent_id)
298
  text_out = json.dumps(res, indent=2)
299
 
300
+ elif name in ("cache_diagnose", "memory_diagnose"):
301
  res = functions.health_check(kv)
302
  text_out = json.dumps(res, indent=2)
303
 
304
+ elif name in ("cache_forget", "memory_forget"):
305
  obs_ids = _parse_mcp_list_arg(args.get("observationIds"))
306
  res = functions.forget(kv, {
307
  "memoryId": args.get("memoryId"),
 
311
  })
312
  text_out = json.dumps(res, indent=2)
313
 
314
+ elif name in ("cache_export", "memory_export"):
315
  res = functions.export_data(kv, {})
316
  text_out = json.dumps(res, indent=2)
317
 
 
327
  folder_path = (
328
  args.get("cwd")
329
  or args.get("project")
330
+ or os.getenv("AGENTCACHE_CWD")
331
  or os.getenv("AGENTMEMORY_CWD")
332
  or "/unknown"
333
  )
 
364
  res = functions.folder_observe(kv, payload)
365
  text_out = json.dumps(res)
366
 
367
+ elif name in ("agent_cache", "agent_remember"):
368
  agent_id = args.get("agentId") or functions.get_agent_id() or "agent"
369
  content = args.get("content")
370
  project = args.get("project")
 
392
  res = functions.remember(kv, payload)
393
  text_out = json.dumps(res)
394
 
395
+ elif name in ("cache_folders", "memory_folders"):
396
  pairs = sorted(
397
  kv.list(KV.folders),
398
  key=lambda x: x.get("lastUpdated", ""),
 
404
  pairs = [p for p in pairs if p.get("agentId") == aid]
405
  text_out = json.dumps(pairs, indent=2)
406
 
407
+ elif name in ("cache_folder_observations", "memory_folder_observations"):
408
  fp = args.get("folderPath", "")
409
  aid = args.get("agentId", "")
410
  if not fp or not aid:
 
420
  )
421
  text_out = json.dumps(obs, indent=2)
422
 
423
+ elif name in ("cache_timeline", "memory_timeline"):
424
  request_aid = args.get("agentId")
425
  if functions.is_agent_scope_isolated():
426
  current_aid = functions.get_agent_id()
 
438
  )
439
  text_out = json.dumps(res, indent=2)
440
 
441
+ elif name in ("cache_dedup", "memory_dedup"):
442
  res = functions.dedup_folder_observations(
443
  kv,
444
  args.get("folderPath") or None,
src/routes/memories.py CHANGED
@@ -18,7 +18,7 @@ memories_bp = Blueprint("memories", __name__)
18
 
19
  def _check_auth():
20
  import hmac
21
- secret = os.getenv("AGENTMEMORY_SECRET")
22
  if not secret:
23
  return None
24
  auth = request.headers.get("Authorization") or request.headers.get("authorization")
@@ -39,6 +39,7 @@ def _get_kv():
39
  # POST /agentmemory/remember
40
  # ---------------------------------------------------------------------------
41
 
 
42
  @memories_bp.route("/agentmemory/remember", methods=["POST"])
43
  def api_remember():
44
  auth_err = _check_auth()
@@ -57,6 +58,7 @@ def api_remember():
57
  # POST /agentmemory/agent/remember
58
  # ---------------------------------------------------------------------------
59
 
 
60
  @memories_bp.route("/agentmemory/agent/remember", methods=["POST"])
61
  def api_agent_remember():
62
  auth_err = _check_auth()
@@ -101,6 +103,7 @@ def api_agent_remember():
101
  # GET /agentmemory/memories
102
  # ---------------------------------------------------------------------------
103
 
 
104
  @memories_bp.route("/agentmemory/memories", methods=["GET"])
105
  def api_memories_list():
106
  auth_err = _check_auth()
@@ -120,6 +123,7 @@ def api_memories_list():
120
  # POST /agentmemory/forget
121
  # ---------------------------------------------------------------------------
122
 
 
123
  @memories_bp.route("/agentmemory/forget", methods=["POST"])
124
  def api_forget():
125
  auth_err = _check_auth()
 
18
 
19
  def _check_auth():
20
  import hmac
21
+ secret = os.getenv("AGENTCACHE_SECRET") or os.getenv("AGENTMEMORY_SECRET")
22
  if not secret:
23
  return None
24
  auth = request.headers.get("Authorization") or request.headers.get("authorization")
 
39
  # POST /agentmemory/remember
40
  # ---------------------------------------------------------------------------
41
 
42
+ @memories_bp.route("/agentcache/remember", methods=["POST"])
43
  @memories_bp.route("/agentmemory/remember", methods=["POST"])
44
  def api_remember():
45
  auth_err = _check_auth()
 
58
  # POST /agentmemory/agent/remember
59
  # ---------------------------------------------------------------------------
60
 
61
+ @memories_bp.route("/agentcache/agent/remember", methods=["POST"])
62
  @memories_bp.route("/agentmemory/agent/remember", methods=["POST"])
63
  def api_agent_remember():
64
  auth_err = _check_auth()
 
103
  # GET /agentmemory/memories
104
  # ---------------------------------------------------------------------------
105
 
106
+ @memories_bp.route("/agentcache/memories", methods=["GET"])
107
  @memories_bp.route("/agentmemory/memories", methods=["GET"])
108
  def api_memories_list():
109
  auth_err = _check_auth()
 
123
  # POST /agentmemory/forget
124
  # ---------------------------------------------------------------------------
125
 
126
+ @memories_bp.route("/agentcache/forget", methods=["POST"])
127
  @memories_bp.route("/agentmemory/forget", methods=["POST"])
128
  def api_forget():
129
  auth_err = _check_auth()
src/routes/migration.py CHANGED
@@ -14,7 +14,7 @@ migration_bp = Blueprint("migration", __name__)
14
 
15
  def _check_auth():
16
  import hmac
17
- secret = os.getenv("AGENTMEMORY_SECRET")
18
  if not secret:
19
  return None
20
  auth = request.headers.get("Authorization") or request.headers.get("authorization")
@@ -32,9 +32,10 @@ def _get_kv():
32
 
33
 
34
  # ---------------------------------------------------------------------------
35
- # POST /agentmemory/migrate
36
  # ---------------------------------------------------------------------------
37
 
 
38
  @migration_bp.route("/agentmemory/migrate", methods=["POST"])
39
  def api_migrate():
40
  auth_err = _check_auth()
 
14
 
15
  def _check_auth():
16
  import hmac
17
+ secret = os.getenv("AGENTCACHE_SECRET") or os.getenv("AGENTMEMORY_SECRET")
18
  if not secret:
19
  return None
20
  auth = request.headers.get("Authorization") or request.headers.get("authorization")
 
32
 
33
 
34
  # ---------------------------------------------------------------------------
35
+ # POST /agentcache/migrate
36
  # ---------------------------------------------------------------------------
37
 
38
+ @migration_bp.route("/agentcache/migrate", methods=["POST"])
39
  @migration_bp.route("/agentmemory/migrate", methods=["POST"])
40
  def api_migrate():
41
  auth_err = _check_auth()
src/routes/observations.py CHANGED
@@ -24,7 +24,7 @@ def _datetime_now_iso() -> str:
24
  def _check_auth():
25
  """Replicate the check_auth() pattern from app.py."""
26
  import hmac
27
- secret = os.getenv("AGENTMEMORY_SECRET")
28
  if not secret:
29
  return None
30
  auth = request.headers.get("Authorization") or request.headers.get("authorization")
@@ -46,6 +46,7 @@ def _get_kv():
46
  # POST /agentmemory/observe (legacy raw hook endpoint + auto-compat shim)
47
  # ---------------------------------------------------------------------------
48
 
 
49
  @observations_bp.route("/agentmemory/observe", methods=["POST"])
50
  def api_observe():
51
  auth_err = _check_auth()
@@ -63,6 +64,7 @@ def api_observe():
63
  body.get("folderPath")
64
  or body.get("cwd")
65
  or body.get("project")
 
66
  or os.getenv("AGENTMEMORY_CWD")
67
  or "/unknown"
68
  )
@@ -114,6 +116,7 @@ def api_observe():
114
  # POST /agentmemory/agent/observe
115
  # ---------------------------------------------------------------------------
116
 
 
117
  @observations_bp.route("/agentmemory/agent/observe", methods=["POST"])
118
  def api_agent_observe():
119
  auth_err = _check_auth()
@@ -161,6 +164,7 @@ def api_agent_observe():
161
  # GET /agentmemory/folders
162
  # ---------------------------------------------------------------------------
163
 
 
164
  @observations_bp.route("/agentmemory/folders", methods=["GET"])
165
  def api_folders():
166
  auth_err = _check_auth()
@@ -182,6 +186,7 @@ def api_folders():
182
  # GET /agentmemory/folder/observations
183
  # ---------------------------------------------------------------------------
184
 
 
185
  @observations_bp.route("/agentmemory/folder/observations", methods=["GET"])
186
  def api_folder_observations():
187
  auth_err = _check_auth()
@@ -207,6 +212,7 @@ def api_folder_observations():
207
  # POST /agentmemory/session/start (legacy compat shim → 200 no-op)
208
  # ---------------------------------------------------------------------------
209
 
 
210
  @observations_bp.route("/agentmemory/session/start", methods=["POST"])
211
  def api_session_start():
212
  """Legacy session/start — clients in the wild still call this.
@@ -230,6 +236,7 @@ def api_session_start():
230
  # POST /agentmemory/session/end (legacy compat shim → 200 no-op)
231
  # ---------------------------------------------------------------------------
232
 
 
233
  @observations_bp.route("/agentmemory/session/end", methods=["POST"])
234
  def api_session_end():
235
  auth_err = _check_auth()
@@ -242,6 +249,7 @@ def api_session_end():
242
  # GET /agentmemory/observations (legacy compat shim)
243
  # ---------------------------------------------------------------------------
244
 
 
245
  @observations_bp.route("/agentmemory/folder/dedup", methods=["POST"])
246
  def api_folder_dedup():
247
  """POST /agentmemory/folder/dedup — remove duplicate observations.
@@ -270,6 +278,7 @@ def api_folder_dedup():
270
  # GET /agentmemory/observations (legacy compat shim)
271
  # ---------------------------------------------------------------------------
272
 
 
273
  @observations_bp.route("/agentmemory/observations", methods=["GET"])
274
  def api_observations_legacy():
275
  """Legacy /observations?sessionId=... shim.
 
24
  def _check_auth():
25
  """Replicate the check_auth() pattern from app.py."""
26
  import hmac
27
+ secret = os.getenv("AGENTCACHE_SECRET") or os.getenv("AGENTMEMORY_SECRET")
28
  if not secret:
29
  return None
30
  auth = request.headers.get("Authorization") or request.headers.get("authorization")
 
46
  # POST /agentmemory/observe (legacy raw hook endpoint + auto-compat shim)
47
  # ---------------------------------------------------------------------------
48
 
49
+ @observations_bp.route("/agentcache/observe", methods=["POST"])
50
  @observations_bp.route("/agentmemory/observe", methods=["POST"])
51
  def api_observe():
52
  auth_err = _check_auth()
 
64
  body.get("folderPath")
65
  or body.get("cwd")
66
  or body.get("project")
67
+ or os.getenv("AGENTCACHE_CWD")
68
  or os.getenv("AGENTMEMORY_CWD")
69
  or "/unknown"
70
  )
 
116
  # POST /agentmemory/agent/observe
117
  # ---------------------------------------------------------------------------
118
 
119
+ @observations_bp.route("/agentcache/agent/observe", methods=["POST"])
120
  @observations_bp.route("/agentmemory/agent/observe", methods=["POST"])
121
  def api_agent_observe():
122
  auth_err = _check_auth()
 
164
  # GET /agentmemory/folders
165
  # ---------------------------------------------------------------------------
166
 
167
+ @observations_bp.route("/agentcache/folders", methods=["GET"])
168
  @observations_bp.route("/agentmemory/folders", methods=["GET"])
169
  def api_folders():
170
  auth_err = _check_auth()
 
186
  # GET /agentmemory/folder/observations
187
  # ---------------------------------------------------------------------------
188
 
189
+ @observations_bp.route("/agentcache/folder/observations", methods=["GET"])
190
  @observations_bp.route("/agentmemory/folder/observations", methods=["GET"])
191
  def api_folder_observations():
192
  auth_err = _check_auth()
 
212
  # POST /agentmemory/session/start (legacy compat shim → 200 no-op)
213
  # ---------------------------------------------------------------------------
214
 
215
+ @observations_bp.route("/agentcache/session/start", methods=["POST"])
216
  @observations_bp.route("/agentmemory/session/start", methods=["POST"])
217
  def api_session_start():
218
  """Legacy session/start — clients in the wild still call this.
 
236
  # POST /agentmemory/session/end (legacy compat shim → 200 no-op)
237
  # ---------------------------------------------------------------------------
238
 
239
+ @observations_bp.route("/agentcache/session/end", methods=["POST"])
240
  @observations_bp.route("/agentmemory/session/end", methods=["POST"])
241
  def api_session_end():
242
  auth_err = _check_auth()
 
249
  # GET /agentmemory/observations (legacy compat shim)
250
  # ---------------------------------------------------------------------------
251
 
252
+ @observations_bp.route("/agentcache/folder/dedup", methods=["POST"])
253
  @observations_bp.route("/agentmemory/folder/dedup", methods=["POST"])
254
  def api_folder_dedup():
255
  """POST /agentmemory/folder/dedup — remove duplicate observations.
 
278
  # GET /agentmemory/observations (legacy compat shim)
279
  # ---------------------------------------------------------------------------
280
 
281
+ @observations_bp.route("/agentcache/observations", methods=["GET"])
282
  @observations_bp.route("/agentmemory/observations", methods=["GET"])
283
  def api_observations_legacy():
284
  """Legacy /observations?sessionId=... shim.
src/routes/search.py CHANGED
@@ -15,7 +15,7 @@ search_bp = Blueprint("search", __name__)
15
 
16
  def _check_auth():
17
  import hmac
18
- secret = os.getenv("AGENTMEMORY_SECRET")
19
  if not secret:
20
  return None
21
  auth = request.headers.get("Authorization") or request.headers.get("authorization")
@@ -33,9 +33,10 @@ def _get_kv():
33
 
34
 
35
  # ---------------------------------------------------------------------------
36
- # POST /agentmemory/search
37
  # ---------------------------------------------------------------------------
38
 
 
39
  @search_bp.route("/agentmemory/search", methods=["POST"])
40
  def api_search():
41
  auth_err = _check_auth()
@@ -58,9 +59,10 @@ def api_search():
58
 
59
 
60
  # ---------------------------------------------------------------------------
61
- # POST /agentmemory/timeline
62
  # ---------------------------------------------------------------------------
63
 
 
64
  @search_bp.route("/agentmemory/timeline", methods=["POST"])
65
  def api_timeline():
66
  auth_err = _check_auth()
 
15
 
16
  def _check_auth():
17
  import hmac
18
+ secret = os.getenv("AGENTCACHE_SECRET") or os.getenv("AGENTMEMORY_SECRET")
19
  if not secret:
20
  return None
21
  auth = request.headers.get("Authorization") or request.headers.get("authorization")
 
33
 
34
 
35
  # ---------------------------------------------------------------------------
36
+ # POST /agentcache/search
37
  # ---------------------------------------------------------------------------
38
 
39
+ @search_bp.route("/agentcache/search", methods=["POST"])
40
  @search_bp.route("/agentmemory/search", methods=["POST"])
41
  def api_search():
42
  auth_err = _check_auth()
 
59
 
60
 
61
  # ---------------------------------------------------------------------------
62
+ # POST /agentcache/timeline
63
  # ---------------------------------------------------------------------------
64
 
65
+ @search_bp.route("/agentcache/timeline", methods=["POST"])
66
  @search_bp.route("/agentmemory/timeline", methods=["POST"])
67
  def api_timeline():
68
  auth_err = _check_auth()
src/storage/scopes.py CHANGED
@@ -2,7 +2,7 @@
2
  src/storage/scopes.py — KV scope registry (A2.3).
3
 
4
  Copied from src/functions.py — do NOT delete the original (backward compat).
5
- The KV class defines all storage scope keys used across agentmemory-python.
6
  """
7
 
8
 
 
2
  src/storage/scopes.py — KV scope registry (A2.3).
3
 
4
  Copied from src/functions.py — do NOT delete the original (backward compat).
5
+ The KV class defines all storage scope keys used across agentcache-python.
6
  """
7
 
8
 
src/viewer/index.html CHANGED
@@ -3,7 +3,7 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>agentmemory viewer</title>
7
  <link rel="icon" type="image/svg+xml" href="/favicon.svg">
8
  <!-- Removed Google Fonts <link> in #323: the viewer CSP is strict
9
  (default-src 'none', style-src 'unsafe-inline', font-src 'self')
@@ -1287,8 +1287,8 @@
1287
  <body>
1288
  <div class="app-header">
1289
  <a class="brand" href="#folders" data-tab-link="folders" aria-label="Open folders view">
1290
- <h1>agentmemory</h1>
1291
- <span class="version">v__AGENTMEMORY_VERSION__</span>
1292
  </a>
1293
  <div class="header-right">
1294
  <span class="dateline" id="dateline"></span>
@@ -1329,7 +1329,7 @@
1329
  </div>
1330
 
1331
  <footer id="viewer-footer" class="viewer-footer">
1332
- <span>agentmemory viewer &middot; <span id="footer-version">loading...</span></span>
1333
  <span class="footer-sep">&middot;</span>
1334
  <a href="https://github.com/rohitg00/agentmemory" target="_blank" rel="noopener">github</a>
1335
  <span class="footer-sep">&middot;</span>
@@ -1338,7 +1338,7 @@
1338
  <a id="footer-feedback" href="#" target="_blank" rel="noopener">report issue &rarr;</a>
1339
  </footer>
1340
 
1341
- <script nonce="__AGENTMEMORY_VIEWER_NONCE__">
1342
  var safeStorage = {
1343
  _mem: {},
1344
  getItem: function(key) {
@@ -1385,10 +1385,10 @@
1385
  document.documentElement.dataset.theme = dark ? 'dark' : 'light';
1386
  var btn = document.getElementById('theme-toggle');
1387
  if (btn) btn.textContent = dark ? 'LIGHT' : 'DARK';
1388
- if (persist) safeStorage.setItem('agentmemory-theme', dark ? 'dark' : 'light');
1389
  }
1390
  window.toggleTheme = function() { applyTheme(!isDarkMode(), true); };
1391
- var savedTheme = safeStorage.getItem('agentmemory-theme');
1392
  if (savedTheme) {
1393
  applyTheme(savedTheme === 'dark', false);
1394
  } else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
@@ -1442,7 +1442,7 @@
1442
  };
1443
  var CB_STATE_COLORS = { closed: 'badge-green', open: 'badge-red', 'half-open': 'badge-yellow' };
1444
  var TAB_IDS = ['folders', 'memories', 'graph', 'timeline', 'tools'];
1445
- var VIEWER_TOKEN_STORAGE_KEY = 'agentmemory-viewer-token';
1446
 
1447
  var state = {
1448
  activeTab: 'folders',
@@ -1539,8 +1539,8 @@
1539
  }
1540
  }
1541
  (function() {
1542
- var autoToken = '__AGENTMEMORY_AUTO_TOKEN__';
1543
- if (autoToken && autoToken.length > 0 && autoToken !== '__AGENTMEMORY_AUTO_TOKEN__') {
1544
  try { sessionStorage.setItem(VIEWER_TOKEN_STORAGE_KEY, autoToken); } catch (_) {}
1545
  }
1546
  })();
@@ -1561,9 +1561,9 @@
1561
  '<div class="viewer-auth-panel">' +
1562
  '<div>' +
1563
  '<div class="viewer-auth-title">Viewer authorization required</div>' +
1564
- '<div id="viewer-auth-desc" class="viewer-auth-desc">Enter <code>AGENTMEMORY_SECRET</code> to unlock viewer API access.</div>' +
1565
  '</div>' +
1566
- '<input id="viewer-auth-token" type="password" autocomplete="off" spellcheck="false" aria-label="AGENTMEMORY_SECRET" aria-describedby="viewer-auth-desc" placeholder="AGENTMEMORY_SECRET" />' +
1567
  '<button class="btn" data-action="save-viewer-token">Unlock</button>' +
1568
  '</div>';
1569
  var input = document.getElementById('viewer-auth-token');
@@ -1578,7 +1578,7 @@
1578
 
1579
  async function api(path, opts) {
1580
  try {
1581
- var url = REST + '/agentmemory/' + path;
1582
  var headers = Object.assign({ 'Cache-Control': 'no-cache' }, (opts && opts.headers) || {});
1583
  var viewerToken = getViewerToken();
1584
  if (viewerToken && !headers.Authorization && !headers.authorization) {
@@ -1673,7 +1673,7 @@
1673
 
1674
  async function loadDashboard() {
1675
  // Dashboard tab was removed; redirect to the Folders tab which is the
1676
- // active landing page backed by real endpoints (/agentmemory/folders).
1677
  switchTab('folders');
1678
  }
1679
 
@@ -1746,7 +1746,7 @@
1746
  el.innerHTML = '<div style="padding:24px;"><div class="skeleton skeleton-card"></div><div class="skeleton skeleton-card"></div></div>';
1747
  try {
1748
  var qp = 'folderPath=' + encodeURIComponent(folderPath) + '&agentId=' + encodeURIComponent(agentId);
1749
- var result = await apiFetchRaw('/agentmemory/folder/observations?' + qp);
1750
  var obs = (result && result.observations) || [];
1751
 
1752
  var html = '<div id="folder-detail-root" style="padding:20px 24px;">';
@@ -1926,7 +1926,7 @@
1926
  if (viewerToken && !headers.Authorization) headers['Authorization'] = 'Bearer ' + viewerToken;
1927
  // Always use REST base so cross-origin ?port= param works correctly
1928
  var url = path.startsWith('http') ? path
1929
- : REST + (path.startsWith('/') ? path : '/agentmemory/' + path);
1930
  var res = await fetch(url, Object.assign({}, opts, { headers: headers }));
1931
  if (!res.ok) throw new Error(res.status + ' ' + res.statusText);
1932
  return res.json();
@@ -1955,7 +1955,7 @@
1955
  state.graph.queryError = null;
1956
  var graphData;
1957
  try {
1958
- graphData = await apiFetchRaw('/agentmemory/graph');
1959
  } catch(err) {
1960
  state.graph.queryError = String(err);
1961
  graphData = { nodes: [], edges: [] };
@@ -2715,7 +2715,7 @@
2715
 
2716
  var html = '<div class="card" style="margin-bottom:12px;padding:12px;background:var(--bg-subtle);">';
2717
  html += '<div style="font-size:13px;color:var(--ink-muted);line-height:1.5;">';
2718
- html += '<strong>Memories</strong> are durable facts, architecture notes, conventions, and lessons saved via <code>memory_remember</code> MCP tool or the <code>/agentmemory/remember</code> endpoint. They survive across sessions and supersede each other as v1, v2, etc. ';
2719
  html += '<span style="color:var(--ink-faint);">Shown: ' + items.length + ' total.</span>';
2720
  html += '</div></div>';
2721
 
@@ -2740,8 +2740,8 @@
2740
  html += '<div class="empty-state">' +
2741
  '<div class="empty-icon">&#128218;</div>' +
2742
  '<div class="empty-title">No memories yet</div>' +
2743
- '<div class="empty-lead">Memories are the distilled facts agentmemory keeps across sessions &mdash; things like file paths, architectural decisions, and user preferences. Hooks capture them automatically during coding sessions; you can also save one directly.</div>' +
2744
- '<pre class="empty-cmd">memory_remember {\n title: "auth uses jose middleware",\n content: "src/middleware/auth.ts handles JWT validation",\n type: "architecture"\n}</pre>' +
2745
  '<div><a class="empty-link" href="https://github.com/rohitg00/agentmemory#memories" target="_blank" rel="noopener">Memory types &rarr;</a></div>' +
2746
  '</div>';
2747
  } else {
@@ -2833,7 +2833,7 @@
2833
  var body = { limit: 100 };
2834
  if (folderPath) body.folderPath = folderPath;
2835
  if (agentId) body.agentId = agentId;
2836
- var result = await apiFetchRaw('/agentmemory/timeline', {
2837
  method: 'POST',
2838
  body: JSON.stringify(body)
2839
  });
@@ -3114,11 +3114,11 @@
3114
  renderObservations();
3115
  }
3116
 
3117
- // loadActivity removed — /agentmemory/sessions endpoint does not exist.
3118
  async function loadActivity() { return; }
3119
  function renderActivity() { return; }
3120
 
3121
- // loadSessions removed — /agentmemory/sessions endpoint does not exist.
3122
  async function loadSessions() { return; }
3123
  function renderSessions() { return; }
3124
  function selectSession(id) { return; }
@@ -3405,7 +3405,7 @@
3405
  title: 'No LLM provider key set',
3406
  keyLabel: 'ANTHROPIC_API_KEY',
3407
  desc: 'Compression, summarization, and graph extraction stay disabled until a key is provided.',
3408
- enable: 'export ANTHROPIC_API_KEY=sk-ant-...\n# then restart: npx @agentmemory/agentmemory',
3409
  docs: 'https://github.com/rohitg00/agentmemory#quick-start',
3410
  dismissKey: '__provider_noop',
3411
  });
@@ -3478,7 +3478,7 @@
3478
  '### Steps to reproduce\n\n' +
3479
  '1. \n2. \n3. \n'
3480
  );
3481
- fbEl.href = 'https://github.com/rohitg00/agentmemory/issues/new?title=' +
3482
  encodeURIComponent('[viewer] ') + '&body=' + body;
3483
  }
3484
  }
@@ -3727,7 +3727,7 @@
3727
  // B4 -- Onboarding card (empty state detection)
3728
  // ==========================================================================
3729
  function maybeShowOnboarding(health, flags) {
3730
- if (safeStorage.getItem('agentmemory_onboarding_dismissed')) return;
3731
  if ((health.observationCount || 0) > 0 || (health.memoryCount || 0) > 0) return;
3732
  var el = document.getElementById('view-folders');
3733
  if (!el) return;
@@ -3748,20 +3748,20 @@
3748
  '</div></div>';
3749
  }
3750
 
3751
- var secretSet = false; // AGENTMEMORY_SECRET status not exposed by flags endpoint
3752
  var geminiSet = !!(flags && flags.embeddingProvider && flags.embeddingProvider !== 'none');
3753
 
3754
  var card = document.createElement('div');
3755
  card.className = 'onboarding-card';
3756
  card.innerHTML =
3757
- '<h2>Welcome to agentmemory</h2>' +
3758
- '<p>No observations yet. Complete these steps to start recording agent memory.</p>' +
3759
- step(1, secretSet, 'Set AGENTMEMORY_SECRET (optional auth)',
3760
- 'echo "AGENTMEMORY_SECRET=your-secret-here" >> ~/.agentmemory/.env') +
3761
  step(2, geminiSet, 'Add GEMINI_API_KEY for semantic search',
3762
- 'echo "GEMINI_API_KEY=your-key" >> ~/.agentmemory/.env') +
3763
- step(3, false, 'Point your agent at agentmemory',
3764
- 'curl -X POST http://localhost:3111/agentmemory/agent/observe \\\n' +
3765
  ' -H "Content-Type: application/json" \\\n' +
3766
  ' -d \'{"folderPath":"/your/project","agentId":"kiro","text":"Hello",' +
3767
  '"timestamp":"' + new Date().toISOString() + '"}\'') +
@@ -3774,7 +3774,7 @@
3774
  });
3775
  });
3776
  card.querySelector('.onboarding-dismiss').addEventListener('click', function() {
3777
- safeStorage.setItem('agentmemory_onboarding_dismissed', '1');
3778
  card.remove();
3779
  });
3780
 
@@ -3789,7 +3789,7 @@
3789
  await _origLoadFolders.apply(this, arguments);
3790
  // After load, check health for onboarding card
3791
  try {
3792
- var h = await apiFetchRaw('/agentmemory/health');
3793
  var f = state.flagsConfig;
3794
  maybeShowOnboarding(h, f);
3795
  } catch(_) {}
@@ -3812,7 +3812,7 @@
3812
  '<div class="skeleton skeleton-card"></div>' +
3813
  '</div>';
3814
  try {
3815
- var data = await apiFetch('/agentmemory/mcp/tools');
3816
  var tools = data.tools || [];
3817
  renderToolsTab(el, tools);
3818
  } catch(err) {
@@ -3890,7 +3890,7 @@
3890
  var _toolToken = getViewerToken();
3891
  var _toolHeaders = { 'Content-Type': 'application/json' };
3892
  if (_toolToken) _toolHeaders['Authorization'] = 'Bearer ' + _toolToken;
3893
- var rawRes = await fetch(REST + '/agentmemory/mcp/tools', {
3894
  method: 'POST',
3895
  headers: _toolHeaders,
3896
  body: JSON.stringify({ name: name, arguments: args })
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>agentcache viewer</title>
7
  <link rel="icon" type="image/svg+xml" href="/favicon.svg">
8
  <!-- Removed Google Fonts <link> in #323: the viewer CSP is strict
9
  (default-src 'none', style-src 'unsafe-inline', font-src 'self')
 
1287
  <body>
1288
  <div class="app-header">
1289
  <a class="brand" href="#folders" data-tab-link="folders" aria-label="Open folders view">
1290
+ <h1>agentcache</h1>
1291
+ <span class="version">v__AGENTCACHE_VERSION__</span>
1292
  </a>
1293
  <div class="header-right">
1294
  <span class="dateline" id="dateline"></span>
 
1329
  </div>
1330
 
1331
  <footer id="viewer-footer" class="viewer-footer">
1332
+ <span>agentcache viewer &middot; <span id="footer-version">loading...</span></span>
1333
  <span class="footer-sep">&middot;</span>
1334
  <a href="https://github.com/rohitg00/agentmemory" target="_blank" rel="noopener">github</a>
1335
  <span class="footer-sep">&middot;</span>
 
1338
  <a id="footer-feedback" href="#" target="_blank" rel="noopener">report issue &rarr;</a>
1339
  </footer>
1340
 
1341
+ <script nonce="__AGENTCACHE_VIEWER_NONCE__">
1342
  var safeStorage = {
1343
  _mem: {},
1344
  getItem: function(key) {
 
1385
  document.documentElement.dataset.theme = dark ? 'dark' : 'light';
1386
  var btn = document.getElementById('theme-toggle');
1387
  if (btn) btn.textContent = dark ? 'LIGHT' : 'DARK';
1388
+ if (persist) safeStorage.setItem('agentcache-theme', dark ? 'dark' : 'light');
1389
  }
1390
  window.toggleTheme = function() { applyTheme(!isDarkMode(), true); };
1391
+ var savedTheme = safeStorage.getItem('agentcache-theme');
1392
  if (savedTheme) {
1393
  applyTheme(savedTheme === 'dark', false);
1394
  } else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
 
1442
  };
1443
  var CB_STATE_COLORS = { closed: 'badge-green', open: 'badge-red', 'half-open': 'badge-yellow' };
1444
  var TAB_IDS = ['folders', 'memories', 'graph', 'timeline', 'tools'];
1445
+ var VIEWER_TOKEN_STORAGE_KEY = 'agentcache-viewer-token';
1446
 
1447
  var state = {
1448
  activeTab: 'folders',
 
1539
  }
1540
  }
1541
  (function() {
1542
+ var autoToken = '__AGENTCACHE_AUTO_TOKEN__';
1543
+ if (autoToken && autoToken.length > 0 && autoToken !== '__AGENTCACHE_AUTO_TOKEN__') {
1544
  try { sessionStorage.setItem(VIEWER_TOKEN_STORAGE_KEY, autoToken); } catch (_) {}
1545
  }
1546
  })();
 
1561
  '<div class="viewer-auth-panel">' +
1562
  '<div>' +
1563
  '<div class="viewer-auth-title">Viewer authorization required</div>' +
1564
+ '<div id="viewer-auth-desc" class="viewer-auth-desc">Enter <code>AGENTCACHE_SECRET</code> to unlock viewer API access.</div>' +
1565
  '</div>' +
1566
+ '<input id="viewer-auth-token" type="password" autocomplete="off" spellcheck="false" aria-label="AGENTCACHE_SECRET" aria-describedby="viewer-auth-desc" placeholder="AGENTCACHE_SECRET" />' +
1567
  '<button class="btn" data-action="save-viewer-token">Unlock</button>' +
1568
  '</div>';
1569
  var input = document.getElementById('viewer-auth-token');
 
1578
 
1579
  async function api(path, opts) {
1580
  try {
1581
+ var url = REST + '/agentcache/' + path;
1582
  var headers = Object.assign({ 'Cache-Control': 'no-cache' }, (opts && opts.headers) || {});
1583
  var viewerToken = getViewerToken();
1584
  if (viewerToken && !headers.Authorization && !headers.authorization) {
 
1673
 
1674
  async function loadDashboard() {
1675
  // Dashboard tab was removed; redirect to the Folders tab which is the
1676
+ // active landing page backed by real endpoints (/agentcache/folders).
1677
  switchTab('folders');
1678
  }
1679
 
 
1746
  el.innerHTML = '<div style="padding:24px;"><div class="skeleton skeleton-card"></div><div class="skeleton skeleton-card"></div></div>';
1747
  try {
1748
  var qp = 'folderPath=' + encodeURIComponent(folderPath) + '&agentId=' + encodeURIComponent(agentId);
1749
+ var result = await apiFetchRaw('/agentcache/folder/observations?' + qp);
1750
  var obs = (result && result.observations) || [];
1751
 
1752
  var html = '<div id="folder-detail-root" style="padding:20px 24px;">';
 
1926
  if (viewerToken && !headers.Authorization) headers['Authorization'] = 'Bearer ' + viewerToken;
1927
  // Always use REST base so cross-origin ?port= param works correctly
1928
  var url = path.startsWith('http') ? path
1929
+ : REST + (path.startsWith('/') ? path : '/agentcache/' + path);
1930
  var res = await fetch(url, Object.assign({}, opts, { headers: headers }));
1931
  if (!res.ok) throw new Error(res.status + ' ' + res.statusText);
1932
  return res.json();
 
1955
  state.graph.queryError = null;
1956
  var graphData;
1957
  try {
1958
+ graphData = await apiFetchRaw('/agentcache/graph');
1959
  } catch(err) {
1960
  state.graph.queryError = String(err);
1961
  graphData = { nodes: [], edges: [] };
 
2715
 
2716
  var html = '<div class="card" style="margin-bottom:12px;padding:12px;background:var(--bg-subtle);">';
2717
  html += '<div style="font-size:13px;color:var(--ink-muted);line-height:1.5;">';
2718
+ html += '<strong>Memories</strong> are durable facts, architecture notes, conventions, and lessons saved via <code>agent_cache</code> MCP tool or the <code>/agentcache/remember</code> endpoint. They survive across sessions and supersede each other as v1, v2, etc. ';
2719
  html += '<span style="color:var(--ink-faint);">Shown: ' + items.length + ' total.</span>';
2720
  html += '</div></div>';
2721
 
 
2740
  html += '<div class="empty-state">' +
2741
  '<div class="empty-icon">&#128218;</div>' +
2742
  '<div class="empty-title">No memories yet</div>' +
2743
+ '<div class="empty-lead">Memories are the distilled facts agentcache keeps across sessions &mdash; things like file paths, architectural decisions, and user preferences. Hooks capture them automatically during coding sessions; you can also save one directly.</div>' +
2744
+ '<pre class="empty-cmd">agent_cache {\n title: "auth uses jose middleware",\n content: "src/middleware/auth.ts handles JWT validation",\n type: "architecture"\n}</pre>' +
2745
  '<div><a class="empty-link" href="https://github.com/rohitg00/agentmemory#memories" target="_blank" rel="noopener">Memory types &rarr;</a></div>' +
2746
  '</div>';
2747
  } else {
 
2833
  var body = { limit: 100 };
2834
  if (folderPath) body.folderPath = folderPath;
2835
  if (agentId) body.agentId = agentId;
2836
+ var result = await apiFetchRaw('/agentcache/timeline', {
2837
  method: 'POST',
2838
  body: JSON.stringify(body)
2839
  });
 
3114
  renderObservations();
3115
  }
3116
 
3117
+ // loadActivity removed — /agentcache/sessions endpoint does not exist.
3118
  async function loadActivity() { return; }
3119
  function renderActivity() { return; }
3120
 
3121
+ // loadSessions removed — /agentcache/sessions endpoint does not exist.
3122
  async function loadSessions() { return; }
3123
  function renderSessions() { return; }
3124
  function selectSession(id) { return; }
 
3405
  title: 'No LLM provider key set',
3406
  keyLabel: 'ANTHROPIC_API_KEY',
3407
  desc: 'Compression, summarization, and graph extraction stay disabled until a key is provided.',
3408
+ enable: 'export ANTHROPIC_API_KEY=sk-ant-...\n# then restart: npx @agentcache/agentcache',
3409
  docs: 'https://github.com/rohitg00/agentmemory#quick-start',
3410
  dismissKey: '__provider_noop',
3411
  });
 
3478
  '### Steps to reproduce\n\n' +
3479
  '1. \n2. \n3. \n'
3480
  );
3481
+ fbEl.href = 'https://github.com/rohitg00/agentcache/issues/new?title=' +
3482
  encodeURIComponent('[viewer] ') + '&body=' + body;
3483
  }
3484
  }
 
3727
  // B4 -- Onboarding card (empty state detection)
3728
  // ==========================================================================
3729
  function maybeShowOnboarding(health, flags) {
3730
+ if (safeStorage.getItem('agentcache_onboarding_dismissed')) return;
3731
  if ((health.observationCount || 0) > 0 || (health.memoryCount || 0) > 0) return;
3732
  var el = document.getElementById('view-folders');
3733
  if (!el) return;
 
3748
  '</div></div>';
3749
  }
3750
 
3751
+ var secretSet = false; // AGENTCACHE_SECRET status not exposed by flags endpoint
3752
  var geminiSet = !!(flags && flags.embeddingProvider && flags.embeddingProvider !== 'none');
3753
 
3754
  var card = document.createElement('div');
3755
  card.className = 'onboarding-card';
3756
  card.innerHTML =
3757
+ '<h2>Welcome to agentcache</h2>' +
3758
+ '<p>No observations yet. Complete these steps to start recording agent cache.</p>' +
3759
+ step(1, secretSet, 'Set AGENTCACHE_SECRET (optional auth)',
3760
+ 'echo "AGENTCACHE_SECRET=your-secret-here" >> ~/.agentcache/.env') +
3761
  step(2, geminiSet, 'Add GEMINI_API_KEY for semantic search',
3762
+ 'echo "GEMINI_API_KEY=your-key" >> ~/.agentcache/.env') +
3763
+ step(3, false, 'Point your agent at agentcache',
3764
+ 'curl -X POST http://localhost:3111/agentcache/agent/observe \\\n' +
3765
  ' -H "Content-Type: application/json" \\\n' +
3766
  ' -d \'{"folderPath":"/your/project","agentId":"kiro","text":"Hello",' +
3767
  '"timestamp":"' + new Date().toISOString() + '"}\'') +
 
3774
  });
3775
  });
3776
  card.querySelector('.onboarding-dismiss').addEventListener('click', function() {
3777
+ safeStorage.setItem('agentcache_onboarding_dismissed', '1');
3778
  card.remove();
3779
  });
3780
 
 
3789
  await _origLoadFolders.apply(this, arguments);
3790
  // After load, check health for onboarding card
3791
  try {
3792
+ var h = await apiFetchRaw('/agentcache/health');
3793
  var f = state.flagsConfig;
3794
  maybeShowOnboarding(h, f);
3795
  } catch(_) {}
 
3812
  '<div class="skeleton skeleton-card"></div>' +
3813
  '</div>';
3814
  try {
3815
+ var data = await apiFetch('/agentcache/mcp/tools');
3816
  var tools = data.tools || [];
3817
  renderToolsTab(el, tools);
3818
  } catch(err) {
 
3890
  var _toolToken = getViewerToken();
3891
  var _toolHeaders = { 'Content-Type': 'application/json' };
3892
  if (_toolToken) _toolHeaders['Authorization'] = 'Bearer ' + _toolToken;
3893
+ var rawRes = await fetch(REST + '/agentcache/mcp/tools', {
3894
  method: 'POST',
3895
  headers: _toolHeaders,
3896
  body: JSON.stringify({ name: name, arguments: args })
src/viewer_helpers.py CHANGED
@@ -23,14 +23,14 @@ def make_viewer_response(base_dir: str):
23
 
24
  nonce = base64.urlsafe_b64encode(secrets.token_bytes(16)).decode("utf-8").rstrip("=")
25
 
26
- # D2.2: Never embed the raw AGENTMEMORY_SECRET in page source.
27
  # Replace the placeholder with an empty string — the viewer authenticates
28
  # via the Authorization header set programmatically after load.
29
  html = (
30
  template
31
- .replace("__AGENTMEMORY_VIEWER_NONCE__", nonce)
32
- .replace("__AGENTMEMORY_VERSION__", "0.9.8")
33
- .replace("__AGENTMEMORY_AUTO_TOKEN__", "")
34
  )
35
 
36
  csp = "; ".join([
 
23
 
24
  nonce = base64.urlsafe_b64encode(secrets.token_bytes(16)).decode("utf-8").rstrip("=")
25
 
26
+ # D2.2: Never embed the raw AGENTCACHE_SECRET in page source.
27
  # Replace the placeholder with an empty string — the viewer authenticates
28
  # via the Authorization header set programmatically after load.
29
  html = (
30
  template
31
+ .replace("__AGENTCACHE_VIEWER_NONCE__", nonce)
32
+ .replace("__AGENTCACHE_VERSION__", "0.9.8")
33
+ .replace("__AGENTCACHE_AUTO_TOKEN__", "")
34
  )
35
 
36
  csp = "; ".join([
src/workers.py CHANGED
@@ -1,5 +1,5 @@
1
  """
2
- Background worker threads for agentmemory-python.
3
 
4
  Start all workers via start_background_workers(kv).
5
  Each worker runs as a daemon thread so it exits automatically when the process dies.
 
1
  """
2
+ Background worker threads for agentcache-python.
3
 
4
  Start all workers via start_background_workers(kv).
5
  Each worker runs as a daemon thread so it exits automatically when the process dies.
start.sh CHANGED
@@ -1,17 +1,17 @@
1
  #!/usr/bin/env bash
2
  # Note: no set -e — sync failures must not kill the container
3
 
4
- # Create agentmemory data directories
5
- mkdir -p /home/user/.agentmemory
6
 
7
  # =============================================================================
8
  # Persistent storage via HF Dataset repo
9
  # Secrets to set in HF Space settings:
10
  # HF_TOKEN — write access to the dataset repo
11
  # GEMINI_API_KEY — powers graph, embeddings, compression, crystals
12
- # AGENTMEMORY_DATASET_REPO — optional override (default: Yash030/agentmemory-python-data)
13
  # =============================================================================
14
- export AGENTMEMORY_DATASET_REPO="${AGENTMEMORY_DATASET_REPO:-Yash030/agentmemory-python-data}"
15
 
16
  echo "[start] Restoring data from HF Dataset..."
17
  python3 /app/sync.py restore
@@ -25,32 +25,32 @@ python3 /app/sync.py restore
25
  ) &
26
 
27
  # Generate HMAC secret on first boot, persist it so it survives dataset restore
28
- HMAC_FILE="/home/user/.agentmemory/.hmac"
29
  if [ ! -s "$HMAC_FILE" ]; then
30
  SECRET="$(openssl rand -hex 32)"
31
  printf '%s\n' "$SECRET" > "$HMAC_FILE"
32
  chmod 600 "$HMAC_FILE"
33
  echo "================================================================"
34
- echo "agentmemory: generated HMAC secret on first boot"
35
- echo "AGENTMEMORY_SECRET=$SECRET"
36
- echo "Copy this to your Space secrets as AGENTMEMORY_SECRET."
37
  echo "It will not be printed again."
38
  echo "================================================================"
39
  fi
40
- export AGENTMEMORY_SECRET="${AGENTMEMORY_SECRET:-$(cat "$HMAC_FILE")}"
41
 
42
  # Write .env config for the daemon so it is loaded by src/app.py
43
- cat > /home/user/.agentmemory/.env <<EOF
44
  GEMINI_API_KEY=${GEMINI_API_KEY}
45
- AGENTMEMORY_SECRET=${AGENTMEMORY_SECRET}
46
- AGENTMEMORY_URL=http://localhost:7860
47
  III_ENGINE_URL=ws://localhost:49134
48
  GEMINI_MODEL=${GEMINI_MODEL:-gemini-2.5-flash}
49
  EMBEDDING_PROVIDER=gemini
50
  CONSOLIDATION_ENABLED=true
51
  GRAPH_EXTRACTION_ENABLED=true
52
- AGENTMEMORY_REFLECT=true
53
- AGENTMEMORY_AUTO_COMPRESS=true
54
  EOF
55
 
56
  # Set the port for Flask application to run on (Hugging Face Space expects 7860)
@@ -64,3 +64,4 @@ python3 src/app.py
64
  # Perform a final backup on shutdown
65
  echo "[start] Flask application exited. Performing final backup..."
66
  python3 /app/sync.py backup
 
 
1
  #!/usr/bin/env bash
2
  # Note: no set -e — sync failures must not kill the container
3
 
4
+ # Create agentcache data directories
5
+ mkdir -p /home/user/.agentcache
6
 
7
  # =============================================================================
8
  # Persistent storage via HF Dataset repo
9
  # Secrets to set in HF Space settings:
10
  # HF_TOKEN — write access to the dataset repo
11
  # GEMINI_API_KEY — powers graph, embeddings, compression, crystals
12
+ # AGENTCACHE_DATASET_REPO — optional override (default: Yash030/agentmemory-python-data)
13
  # =============================================================================
14
+ export AGENTCACHE_DATASET_REPO="${AGENTCACHE_DATASET_REPO:-${AGENTMEMORY_DATASET_REPO:-Yash030/agentmemory-python-data}}"
15
 
16
  echo "[start] Restoring data from HF Dataset..."
17
  python3 /app/sync.py restore
 
25
  ) &
26
 
27
  # Generate HMAC secret on first boot, persist it so it survives dataset restore
28
+ HMAC_FILE="/home/user/.agentcache/.hmac"
29
  if [ ! -s "$HMAC_FILE" ]; then
30
  SECRET="$(openssl rand -hex 32)"
31
  printf '%s\n' "$SECRET" > "$HMAC_FILE"
32
  chmod 600 "$HMAC_FILE"
33
  echo "================================================================"
34
+ echo "agentcache: generated HMAC secret on first boot"
35
+ echo "AGENTCACHE_SECRET=$SECRET"
36
+ echo "Copy this to your Space secrets as AGENTCACHE_SECRET."
37
  echo "It will not be printed again."
38
  echo "================================================================"
39
  fi
40
+ export AGENTCACHE_SECRET="${AGENTCACHE_SECRET:-${AGENTMEMORY_SECRET:-$(cat "$HMAC_FILE")}}"
41
 
42
  # Write .env config for the daemon so it is loaded by src/app.py
43
+ cat > /home/user/.agentcache/.env <<EOF
44
  GEMINI_API_KEY=${GEMINI_API_KEY}
45
+ AGENTCACHE_SECRET=${AGENTCACHE_SECRET}
46
+ AGENTCACHE_URL=http://localhost:7860
47
  III_ENGINE_URL=ws://localhost:49134
48
  GEMINI_MODEL=${GEMINI_MODEL:-gemini-2.5-flash}
49
  EMBEDDING_PROVIDER=gemini
50
  CONSOLIDATION_ENABLED=true
51
  GRAPH_EXTRACTION_ENABLED=true
52
+ AGENTCACHE_REFLECT=true
53
+ AGENTCACHE_AUTO_COMPRESS=true
54
  EOF
55
 
56
  # Set the port for Flask application to run on (Hugging Face Space expects 7860)
 
64
  # Perform a final backup on shutdown
65
  echo "[start] Flask application exited. Performing final backup..."
66
  python3 /app/sync.py backup
67
+
sync.py CHANGED
@@ -1,6 +1,6 @@
1
  #!/usr/bin/env python3
2
  """
3
- Sync agentmemory data to/from a private HF Dataset repo.
4
  Usage:
5
  python3 sync.py restore -- download DB from HF on startup
6
  python3 sync.py backup -- upload DB to HF (called in loop)
@@ -24,13 +24,13 @@ except ImportError:
24
  sys.exit(0)
25
 
26
  HF_TOKEN = os.environ.get("HF_TOKEN", "")
27
- REPO_ID = os.environ.get("AGENTMEMORY_DATASET_REPO", "Yash030/agentmemory-python-data")
28
- DATA_DIR = os.path.expanduser("~/.agentmemory")
29
- DB_PATH = os.path.join(DATA_DIR, "agentmemory.db")
30
 
31
  # Only these paths are backed up/restored — everything else is ephemeral
32
  SYNC_FILES = [
33
- "agentmemory.db",
34
  ".hmac",
35
  ]
36
  SYNC_DIRS = [
@@ -232,7 +232,7 @@ def backup():
232
  return
233
 
234
  # Stage only the targeted files
235
- staging = tempfile.mkdtemp(prefix="agentmemory_sync_")
236
  try:
237
  for full, rel in targets:
238
  dest = os.path.join(staging, rel.replace("/", os.sep))
 
1
  #!/usr/bin/env python3
2
  """
3
+ Sync agentcache data to/from a private HF Dataset repo.
4
  Usage:
5
  python3 sync.py restore -- download DB from HF on startup
6
  python3 sync.py backup -- upload DB to HF (called in loop)
 
24
  sys.exit(0)
25
 
26
  HF_TOKEN = os.environ.get("HF_TOKEN", "")
27
+ REPO_ID = os.environ.get("AGENTCACHE_DATASET_REPO") or os.environ.get("AGENTMEMORY_DATASET_REPO") or "Yash030/agentmemory-python-data"
28
+ DATA_DIR = os.path.expanduser("~/.agentcache")
29
+ DB_PATH = os.path.join(DATA_DIR, "agentcache.db")
30
 
31
  # Only these paths are backed up/restored — everything else is ephemeral
32
  SYNC_FILES = [
33
+ "agentcache.db",
34
  ".hmac",
35
  ]
36
  SYNC_DIRS = [
 
232
  return
233
 
234
  # Stage only the targeted files
235
+ staging = tempfile.mkdtemp(prefix="agentcache_sync_")
236
  try:
237
  for full, rel in targets:
238
  dest = os.path.join(staging, rel.replace("/", os.sep))
tests/test_api.py CHANGED
@@ -21,6 +21,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
21
  def flask_app(tmp_path_factory):
22
  tmp_dir = tmp_path_factory.mktemp("api_test_db")
23
  db_path = str(tmp_dir / "test.db")
 
24
  os.environ.pop("AGENTMEMORY_SECRET", None)
25
 
26
  from db import StateKV
@@ -31,6 +32,8 @@ def flask_app(tmp_path_factory):
31
 
32
  StateKV.__init__ = patched_init
33
  import app as app_module
 
 
34
  flask_application = app_module.create_app()
35
  StateKV.__init__ = original_init
36
  flask_application.config["TESTING"] = True
@@ -51,12 +54,12 @@ def _post(client, url, payload):
51
 
52
 
53
  # ---------------------------------------------------------------------------
54
- # POST /agentmemory/agent/observe
55
  # ---------------------------------------------------------------------------
56
 
57
  class TestAgentObserve:
58
  def test_valid_payload_returns_201(self, client):
59
- resp = _post(client, "/agentmemory/agent/observe", {
60
  "folderPath": "/home/user/test-project",
61
  "agentId": "kiro",
62
  "text": "Implemented new authentication middleware",
@@ -68,7 +71,7 @@ class TestAgentObserve:
68
  assert data["observationId"].startswith("fobs_")
69
 
70
  def test_missing_folder_path_returns_400(self, client):
71
- resp = _post(client, "/agentmemory/agent/observe", {
72
  "agentId": "kiro",
73
  "text": "Some work",
74
  "timestamp": _now(),
@@ -76,7 +79,7 @@ class TestAgentObserve:
76
  assert resp.status_code == 400
77
 
78
  def test_missing_agent_id_returns_400(self, client):
79
- resp = _post(client, "/agentmemory/agent/observe", {
80
  "folderPath": "/home/user/proj",
81
  "text": "Some work",
82
  "timestamp": _now(),
@@ -84,7 +87,7 @@ class TestAgentObserve:
84
  assert resp.status_code == 400
85
 
86
  def test_missing_text_returns_400(self, client):
87
- resp = _post(client, "/agentmemory/agent/observe", {
88
  "folderPath": "/home/user/proj",
89
  "agentId": "kiro",
90
  "timestamp": _now(),
@@ -93,46 +96,46 @@ class TestAgentObserve:
93
 
94
 
95
  # ---------------------------------------------------------------------------
96
- # POST /agentmemory/search
97
  # ---------------------------------------------------------------------------
98
 
99
  class TestSearch:
100
  def test_search_with_query_returns_200(self, client):
101
  # Seed data first
102
- _post(client, "/agentmemory/agent/observe", {
103
  "folderPath": "/home/user/search-proj",
104
  "agentId": "kiro",
105
  "text": "Refactored the authentication system",
106
  "timestamp": _now(),
107
  })
108
- resp = _post(client, "/agentmemory/search", {"query": "authentication"})
109
  assert resp.status_code == 200
110
  data = resp.get_json()
111
  assert isinstance(data, list) or isinstance(data, dict)
112
 
113
  def test_search_missing_query_returns_400(self, client):
114
- resp = _post(client, "/agentmemory/search", {})
115
  assert resp.status_code == 400
116
 
117
  def test_search_empty_query_returns_400(self, client):
118
- resp = _post(client, "/agentmemory/search", {"query": " "})
119
  assert resp.status_code == 400
120
 
121
 
122
  # ---------------------------------------------------------------------------
123
- # GET /agentmemory/folders
124
  # ---------------------------------------------------------------------------
125
 
126
  class TestFolders:
127
  def test_get_folders_returns_200(self, client):
128
  # Ensure at least one folder exists from earlier tests
129
- _post(client, "/agentmemory/agent/observe", {
130
  "folderPath": "/home/user/folders-check",
131
  "agentId": "kiro",
132
  "text": "Check folders endpoint",
133
  "timestamp": _now(),
134
  })
135
- resp = client.get("/agentmemory/folders")
136
  assert resp.status_code == 200
137
  data = resp.get_json()
138
  assert "folders" in data
@@ -140,12 +143,12 @@ class TestFolders:
140
 
141
 
142
  # ---------------------------------------------------------------------------
143
- # GET /agentmemory/health
144
  # ---------------------------------------------------------------------------
145
 
146
  class TestHealth:
147
  def test_health_returns_200(self, client):
148
- resp = client.get("/agentmemory/health")
149
  assert resp.status_code == 200
150
  data = resp.get_json()
151
  assert "folderCount" in data
@@ -153,29 +156,29 @@ class TestHealth:
153
  assert "memoryCount" in data
154
 
155
  def test_health_status_ok(self, client):
156
- resp = client.get("/agentmemory/health")
157
  data = resp.get_json()
158
  assert data.get("status") in ("ok", "degraded")
159
 
160
 
161
  # ---------------------------------------------------------------------------
162
- # GET /agentmemory/livez
163
  # ---------------------------------------------------------------------------
164
 
165
  class TestLivez:
166
  def test_livez_returns_200_no_auth(self, client):
167
- resp = client.get("/agentmemory/livez")
168
  assert resp.status_code == 200
169
  data = resp.get_json()
170
  assert data["status"] == "ok"
171
 
172
  def test_livez_open_with_secret_set(self, client):
173
- os.environ["AGENTMEMORY_SECRET"] = "test-secret-123"
174
  try:
175
- resp = client.get("/agentmemory/livez")
176
  assert resp.status_code == 200
177
  finally:
178
- del os.environ["AGENTMEMORY_SECRET"]
179
 
180
 
181
  # ---------------------------------------------------------------------------
@@ -184,35 +187,35 @@ class TestLivez:
184
 
185
  class TestAuthentication:
186
  def test_protected_endpoint_returns_401_with_wrong_token(self, client):
187
- os.environ["AGENTMEMORY_SECRET"] = "correct-secret"
188
  try:
189
  resp = client.get(
190
- "/agentmemory/audit",
191
  headers={"Authorization": "Bearer wrong-token"},
192
  )
193
  assert resp.status_code == 401
194
  finally:
195
- del os.environ["AGENTMEMORY_SECRET"]
196
 
197
  def test_protected_endpoint_passes_with_correct_token(self, client):
198
  secret = "my-test-secret-xyz"
199
- os.environ["AGENTMEMORY_SECRET"] = secret
200
  try:
201
  resp = client.get(
202
- "/agentmemory/audit",
203
  headers={"Authorization": f"Bearer {secret}"},
204
  )
205
  assert resp.status_code == 200
206
  finally:
207
- del os.environ["AGENTMEMORY_SECRET"]
208
 
209
  def test_livez_always_open_regardless_of_secret(self, client):
210
- os.environ["AGENTMEMORY_SECRET"] = "any-secret"
211
  try:
212
- resp = client.get("/agentmemory/livez")
213
  assert resp.status_code == 200
214
  finally:
215
- del os.environ["AGENTMEMORY_SECRET"]
216
 
217
 
218
  # ---------------------------------------------------------------------------
@@ -221,34 +224,34 @@ class TestAuthentication:
221
 
222
  class TestMemoriesEndpoint:
223
  def test_memories_list_returns_200(self, client):
224
- resp = client.get("/agentmemory/memories")
225
  assert resp.status_code == 200
226
  data = resp.get_json()
227
  assert "memories" in data
228
 
229
  def test_remember_valid_payload_returns_201(self, client):
230
- resp = _post(client, "/agentmemory/remember", {
231
  "content": "API test memory content",
232
  "type": "fact",
233
  })
234
  assert resp.status_code == 201
235
 
236
  def test_forget_nonexistent_id(self, client):
237
- resp = _post(client, "/agentmemory/forget", {"memoryId": "mem_nonexistent"})
238
  assert resp.status_code == 200
239
 
240
  def test_graph_endpoint_returns_200(self, client):
241
- resp = client.get("/agentmemory/graph")
242
  assert resp.status_code == 200
243
  data = resp.get_json()
244
  assert "nodes" in data
245
  assert "edges" in data
246
 
247
  def test_mcp_tools_list_returns_200(self, client):
248
- resp = client.get("/agentmemory/mcp/tools")
249
  assert resp.status_code == 200
250
  data = resp.get_json()
251
  assert "tools" in data
252
  tool_names = {t["name"] for t in data["tools"]}
253
  assert "agent_observe" in tool_names
254
- assert "memory_recall" in tool_names
 
21
  def flask_app(tmp_path_factory):
22
  tmp_dir = tmp_path_factory.mktemp("api_test_db")
23
  db_path = str(tmp_dir / "test.db")
24
+ os.environ.pop("AGENTCACHE_SECRET", None)
25
  os.environ.pop("AGENTMEMORY_SECRET", None)
26
 
27
  from db import StateKV
 
32
 
33
  StateKV.__init__ = patched_init
34
  import app as app_module
35
+ os.environ.pop("AGENTCACHE_SECRET", None)
36
+ os.environ.pop("AGENTMEMORY_SECRET", None)
37
  flask_application = app_module.create_app()
38
  StateKV.__init__ = original_init
39
  flask_application.config["TESTING"] = True
 
54
 
55
 
56
  # ---------------------------------------------------------------------------
57
+ # POST /agentcache/agent/observe
58
  # ---------------------------------------------------------------------------
59
 
60
  class TestAgentObserve:
61
  def test_valid_payload_returns_201(self, client):
62
+ resp = _post(client, "/agentcache/agent/observe", {
63
  "folderPath": "/home/user/test-project",
64
  "agentId": "kiro",
65
  "text": "Implemented new authentication middleware",
 
71
  assert data["observationId"].startswith("fobs_")
72
 
73
  def test_missing_folder_path_returns_400(self, client):
74
+ resp = _post(client, "/agentcache/agent/observe", {
75
  "agentId": "kiro",
76
  "text": "Some work",
77
  "timestamp": _now(),
 
79
  assert resp.status_code == 400
80
 
81
  def test_missing_agent_id_returns_400(self, client):
82
+ resp = _post(client, "/agentcache/agent/observe", {
83
  "folderPath": "/home/user/proj",
84
  "text": "Some work",
85
  "timestamp": _now(),
 
87
  assert resp.status_code == 400
88
 
89
  def test_missing_text_returns_400(self, client):
90
+ resp = _post(client, "/agentcache/agent/observe", {
91
  "folderPath": "/home/user/proj",
92
  "agentId": "kiro",
93
  "timestamp": _now(),
 
96
 
97
 
98
  # ---------------------------------------------------------------------------
99
+ # POST /agentcache/search
100
  # ---------------------------------------------------------------------------
101
 
102
  class TestSearch:
103
  def test_search_with_query_returns_200(self, client):
104
  # Seed data first
105
+ _post(client, "/agentcache/agent/observe", {
106
  "folderPath": "/home/user/search-proj",
107
  "agentId": "kiro",
108
  "text": "Refactored the authentication system",
109
  "timestamp": _now(),
110
  })
111
+ resp = _post(client, "/agentcache/search", {"query": "authentication"})
112
  assert resp.status_code == 200
113
  data = resp.get_json()
114
  assert isinstance(data, list) or isinstance(data, dict)
115
 
116
  def test_search_missing_query_returns_400(self, client):
117
+ resp = _post(client, "/agentcache/search", {})
118
  assert resp.status_code == 400
119
 
120
  def test_search_empty_query_returns_400(self, client):
121
+ resp = _post(client, "/agentcache/search", {"query": " "})
122
  assert resp.status_code == 400
123
 
124
 
125
  # ---------------------------------------------------------------------------
126
+ # GET /agentcache/folders
127
  # ---------------------------------------------------------------------------
128
 
129
  class TestFolders:
130
  def test_get_folders_returns_200(self, client):
131
  # Ensure at least one folder exists from earlier tests
132
+ _post(client, "/agentcache/agent/observe", {
133
  "folderPath": "/home/user/folders-check",
134
  "agentId": "kiro",
135
  "text": "Check folders endpoint",
136
  "timestamp": _now(),
137
  })
138
+ resp = client.get("/agentcache/folders")
139
  assert resp.status_code == 200
140
  data = resp.get_json()
141
  assert "folders" in data
 
143
 
144
 
145
  # ---------------------------------------------------------------------------
146
+ # GET /agentcache/health
147
  # ---------------------------------------------------------------------------
148
 
149
  class TestHealth:
150
  def test_health_returns_200(self, client):
151
+ resp = client.get("/agentcache/health")
152
  assert resp.status_code == 200
153
  data = resp.get_json()
154
  assert "folderCount" in data
 
156
  assert "memoryCount" in data
157
 
158
  def test_health_status_ok(self, client):
159
+ resp = client.get("/agentcache/health")
160
  data = resp.get_json()
161
  assert data.get("status") in ("ok", "degraded")
162
 
163
 
164
  # ---------------------------------------------------------------------------
165
+ # GET /agentcache/livez
166
  # ---------------------------------------------------------------------------
167
 
168
  class TestLivez:
169
  def test_livez_returns_200_no_auth(self, client):
170
+ resp = client.get("/agentcache/livez")
171
  assert resp.status_code == 200
172
  data = resp.get_json()
173
  assert data["status"] == "ok"
174
 
175
  def test_livez_open_with_secret_set(self, client):
176
+ os.environ["AGENTCACHE_SECRET"] = "test-secret-123"
177
  try:
178
+ resp = client.get("/agentcache/livez")
179
  assert resp.status_code == 200
180
  finally:
181
+ del os.environ["AGENTCACHE_SECRET"]
182
 
183
 
184
  # ---------------------------------------------------------------------------
 
187
 
188
  class TestAuthentication:
189
  def test_protected_endpoint_returns_401_with_wrong_token(self, client):
190
+ os.environ["AGENTCACHE_SECRET"] = "correct-secret"
191
  try:
192
  resp = client.get(
193
+ "/agentcache/audit",
194
  headers={"Authorization": "Bearer wrong-token"},
195
  )
196
  assert resp.status_code == 401
197
  finally:
198
+ del os.environ["AGENTCACHE_SECRET"]
199
 
200
  def test_protected_endpoint_passes_with_correct_token(self, client):
201
  secret = "my-test-secret-xyz"
202
+ os.environ["AGENTCACHE_SECRET"] = secret
203
  try:
204
  resp = client.get(
205
+ "/agentcache/audit",
206
  headers={"Authorization": f"Bearer {secret}"},
207
  )
208
  assert resp.status_code == 200
209
  finally:
210
+ del os.environ["AGENTCACHE_SECRET"]
211
 
212
  def test_livez_always_open_regardless_of_secret(self, client):
213
+ os.environ["AGENTCACHE_SECRET"] = "any-secret"
214
  try:
215
+ resp = client.get("/agentcache/livez")
216
  assert resp.status_code == 200
217
  finally:
218
+ del os.environ["AGENTCACHE_SECRET"]
219
 
220
 
221
  # ---------------------------------------------------------------------------
 
224
 
225
  class TestMemoriesEndpoint:
226
  def test_memories_list_returns_200(self, client):
227
+ resp = client.get("/agentcache/memories")
228
  assert resp.status_code == 200
229
  data = resp.get_json()
230
  assert "memories" in data
231
 
232
  def test_remember_valid_payload_returns_201(self, client):
233
+ resp = _post(client, "/agentcache/remember", {
234
  "content": "API test memory content",
235
  "type": "fact",
236
  })
237
  assert resp.status_code == 201
238
 
239
  def test_forget_nonexistent_id(self, client):
240
+ resp = _post(client, "/agentcache/forget", {"memoryId": "mem_nonexistent"})
241
  assert resp.status_code == 200
242
 
243
  def test_graph_endpoint_returns_200(self, client):
244
+ resp = client.get("/agentcache/graph")
245
  assert resp.status_code == 200
246
  data = resp.get_json()
247
  assert "nodes" in data
248
  assert "edges" in data
249
 
250
  def test_mcp_tools_list_returns_200(self, client):
251
+ resp = client.get("/agentcache/mcp/tools")
252
  assert resp.status_code == 200
253
  data = resp.get_json()
254
  assert "tools" in data
255
  tool_names = {t["name"] for t in data["tools"]}
256
  assert "agent_observe" in tool_names
257
+ assert "cache_recall" in tool_names
tests/test_context.py CHANGED
@@ -18,7 +18,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
18
 
19
  def _make_kv(tmp_path):
20
  from db import StateKV
21
- os.environ.pop("AGENTMEMORY_SECRET", None)
22
  return StateKV(db_path=str(tmp_path / "test.db"))
23
 
24
 
@@ -83,7 +83,7 @@ class TestContext:
83
  assert result["tokens"] <= budget + 50 # small headroom for header/footer
84
 
85
  def test_context_includes_xml_wrapper(self, tmp_path):
86
- """Non-empty context should be wrapped in <agentmemory-context>."""
87
  from functions import context, lesson_save
88
  kv = _make_kv(tmp_path)
89
  project = "/home/user/xml-test"
@@ -101,8 +101,8 @@ class TestContext:
101
  })
102
 
103
  if result["blocks"] > 0:
104
- assert "<agentmemory-context" in result["context"]
105
- assert "</agentmemory-context>" in result["context"]
106
 
107
  def test_token_budget_env_var_respected(self, tmp_path, monkeypatch):
108
  """TOKEN_BUDGET env var should be used when no budget param given."""
 
18
 
19
  def _make_kv(tmp_path):
20
  from db import StateKV
21
+ os.environ.pop("AGENTCACHE_SECRET", None)
22
  return StateKV(db_path=str(tmp_path / "test.db"))
23
 
24
 
 
83
  assert result["tokens"] <= budget + 50 # small headroom for header/footer
84
 
85
  def test_context_includes_xml_wrapper(self, tmp_path):
86
+ """Non-empty context should be wrapped in <agentcache-context>."""
87
  from functions import context, lesson_save
88
  kv = _make_kv(tmp_path)
89
  project = "/home/user/xml-test"
 
101
  })
102
 
103
  if result["blocks"] > 0:
104
+ assert "<agentcache-context" in result["context"]
105
+ assert "</agentcache-context>" in result["context"]
106
 
107
  def test_token_budget_env_var_respected(self, tmp_path, monkeypatch):
108
  """TOKEN_BUDGET env var should be used when no budget param given."""
tests/test_folder_observe.py CHANGED
@@ -70,8 +70,8 @@ class TestFolderObserveSuccess:
70
 
71
  def test_obs_count_incremented(self, tmp_path):
72
  kv = make_kv(tmp_path)
73
- folder_observe(kv, base_payload())
74
- folder_observe(kv, base_payload())
75
  fp = 'home/user/projects/myapp'
76
  meta = kv.get(KV.folder_meta(fp, 'kiro'), 'meta')
77
  assert meta is not None
@@ -99,10 +99,10 @@ class TestFolderObserveCap:
99
  def test_cap_enforced(self, tmp_path, monkeypatch):
100
  monkeypatch.setenv('MAX_OBS_PER_FOLDER', '3')
101
  kv = make_kv(tmp_path)
102
- for _ in range(3):
103
- folder_observe(kv, base_payload())
104
  with pytest.raises(ValueError, match='limit'):
105
- folder_observe(kv, base_payload())
106
 
107
 
108
  class TestFolderObservePairIsolation:
 
70
 
71
  def test_obs_count_incremented(self, tmp_path):
72
  kv = make_kv(tmp_path)
73
+ folder_observe(kv, base_payload(text="First observation"))
74
+ folder_observe(kv, base_payload(text="Second observation"))
75
  fp = 'home/user/projects/myapp'
76
  meta = kv.get(KV.folder_meta(fp, 'kiro'), 'meta')
77
  assert meta is not None
 
99
  def test_cap_enforced(self, tmp_path, monkeypatch):
100
  monkeypatch.setenv('MAX_OBS_PER_FOLDER', '3')
101
  kv = make_kv(tmp_path)
102
+ for i in range(3):
103
+ folder_observe(kv, base_payload(text=f"observation {i}"))
104
  with pytest.raises(ValueError, match='limit'):
105
+ folder_observe(kv, base_payload(text="observation 4"))
106
 
107
 
108
  class TestFolderObservePairIsolation:
tests/test_properties.py CHANGED
@@ -38,7 +38,7 @@ _counter = [0]
38
  def _fresh_kv():
39
  """Create a brand-new isolated StateKV in a temp directory."""
40
  from db import StateKV
41
- os.environ.pop("AGENTMEMORY_SECRET", None)
42
  _counter[0] += 1
43
  d = tempfile.mkdtemp(prefix=f"agmem_prop_{_counter[0]}_")
44
  return StateKV(db_path=os.path.join(d, "test.db"))
 
38
  def _fresh_kv():
39
  """Create a brand-new isolated StateKV in a temp directory."""
40
  from db import StateKV
41
+ os.environ.pop("AGENTCACHE_SECRET", None)
42
  _counter[0] += 1
43
  d = tempfile.mkdtemp(prefix=f"agmem_prop_{_counter[0]}_")
44
  return StateKV(db_path=os.path.join(d, "test.db"))
tests/test_remember.py CHANGED
@@ -18,7 +18,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
18
 
19
  def _make_kv(tmp_path):
20
  from db import StateKV
21
- os.environ.pop("AGENTMEMORY_SECRET", None)
22
  return StateKV(db_path=str(tmp_path / "test.db"))
23
 
24
 
 
18
 
19
  def _make_kv(tmp_path):
20
  from db import StateKV
21
+ os.environ.pop("AGENTCACHE_SECRET", None)
22
  return StateKV(db_path=str(tmp_path / "test.db"))
23
 
24
 
tests/test_route_regressions.py CHANGED
@@ -6,7 +6,7 @@ the expected HTTP status codes after the monolithic app.py was split into
6
  src/routes/{observations, memories, search, graph, health, mcp, migration}.py.
7
 
8
  Tests use the Flask test client — no running server required.
9
- The AGENTMEMORY_SECRET env var is NOT set so all auth checks pass through.
10
  """
11
  import sys
12
  import os
@@ -36,11 +36,14 @@ def flask_app(tmp_path_factory):
36
  db_path = str(tmp_dir / "test.db")
37
 
38
  # Point the server at an isolated DB
39
- os.environ["AGENTMEMORY_DB_PATH"] = db_path
40
  # Ensure no auth requirement during tests
 
41
  os.environ.pop("AGENTMEMORY_SECRET", None)
42
 
43
  import app as app_module
 
 
44
  from db import StateKV
45
 
46
  # Patch StateKV to use tmp db before create_app() initialises it
@@ -81,28 +84,28 @@ def _post_json(client, url, payload):
81
 
82
  # ===========================================================================
83
  # Blueprint 1: health.py
84
- # GET /agentmemory/livez
85
- # GET /agentmemory/health
86
- # GET /agentmemory/audit
87
- # GET /agentmemory/config/flags
88
  # ===========================================================================
89
 
90
  class TestHealthBlueprint:
91
  def test_livez_no_auth_required(self, client):
92
  """GET /livez must respond 200 without any auth token (always open)."""
93
- resp = client.get("/agentmemory/livez")
94
  assert resp.status_code == 200
95
  data = resp.get_json()
96
  assert data["status"] == "ok"
97
  assert "service" in data
98
 
99
  def test_livez_returns_service_name(self, client):
100
- resp = client.get("/agentmemory/livez")
101
  data = resp.get_json()
102
- assert data["service"] == "agentmemory"
103
 
104
  def test_health_returns_200(self, client):
105
- resp = client.get("/agentmemory/health")
106
  assert resp.status_code == 200
107
  data = resp.get_json()
108
  # Folder-based health check fields (REQ-047)
@@ -111,13 +114,13 @@ class TestHealthBlueprint:
111
  assert "memoryCount" in data
112
 
113
  def test_audit_returns_200(self, client):
114
- resp = client.get("/agentmemory/audit")
115
  assert resp.status_code == 200
116
  data = resp.get_json()
117
  assert "entries" in data
118
 
119
  def test_config_flags_returns_200(self, client):
120
- resp = client.get("/agentmemory/config/flags")
121
  assert resp.status_code == 200
122
  data = resp.get_json()
123
  assert "flags" in data
@@ -126,10 +129,10 @@ class TestHealthBlueprint:
126
 
127
  # ===========================================================================
128
  # Blueprint 2: observations.py
129
- # POST /agentmemory/observe
130
- # POST /agentmemory/agent/observe
131
- # GET /agentmemory/folders
132
- # GET /agentmemory/folder/observations
133
  # ===========================================================================
134
 
135
  class TestObservationsBlueprint:
@@ -141,7 +144,7 @@ class TestObservationsBlueprint:
141
  "text": "Added a new feature to the app",
142
  "timestamp": _now_iso(),
143
  }
144
- resp = _post_json(client, "/agentmemory/agent/observe", payload)
145
  assert resp.status_code == 201
146
  data = resp.get_json()
147
  assert "observationId" in data
@@ -150,31 +153,31 @@ class TestObservationsBlueprint:
150
  def test_agent_observe_missing_folder_path_returns_400(self, client):
151
  """POST /agent/observe missing folderPath returns 400."""
152
  payload = {"agentId": "kiro", "text": "some work", "timestamp": _now_iso()}
153
- resp = _post_json(client, "/agentmemory/agent/observe", payload)
154
  assert resp.status_code == 400
155
 
156
  def test_agent_observe_missing_agent_id_returns_400(self, client):
157
  """POST /agent/observe missing agentId returns 400."""
158
  payload = {"folderPath": "/home/user/proj", "text": "some work", "timestamp": _now_iso()}
159
- resp = _post_json(client, "/agentmemory/agent/observe", payload)
160
  assert resp.status_code == 400
161
 
162
  def test_agent_observe_missing_text_returns_400(self, client):
163
  """POST /agent/observe missing text returns 400."""
164
  payload = {"folderPath": "/home/user/proj", "agentId": "kiro", "timestamp": _now_iso()}
165
- resp = _post_json(client, "/agentmemory/agent/observe", payload)
166
  assert resp.status_code == 400
167
 
168
  def test_folders_list_returns_200(self, client):
169
  """GET /folders returns 200 with a folders list."""
170
  # Seed at least one observation first
171
- _post_json(client, "/agentmemory/agent/observe", {
172
  "folderPath": "/home/user/proj-folders-test",
173
  "agentId": "kiro",
174
  "text": "Folders test observation",
175
  "timestamp": _now_iso(),
176
  })
177
- resp = client.get("/agentmemory/folders")
178
  assert resp.status_code == 200
179
  data = resp.get_json()
180
  assert "folders" in data
@@ -184,20 +187,20 @@ class TestObservationsBlueprint:
184
  """GET /folder/observations with valid params returns 200."""
185
  # Seed an observation first
186
  fp = "/home/user/proj-obs-test"
187
- _post_json(client, "/agentmemory/agent/observe", {
188
  "folderPath": fp,
189
  "agentId": "kiro",
190
  "text": "Obs for folder/observations test",
191
  "timestamp": _now_iso(),
192
  })
193
- resp = client.get("/agentmemory/folder/observations?folderPath=home/user/proj-obs-test&agentId=kiro")
194
  assert resp.status_code == 200
195
  data = resp.get_json()
196
  assert "observations" in data
197
 
198
  def test_folder_observations_missing_params_returns_400(self, client):
199
  """GET /folder/observations without required params returns 400."""
200
- resp = client.get("/agentmemory/folder/observations")
201
  assert resp.status_code == 400
202
 
203
  def test_legacy_observe_endpoint_returns_400_or_201(self, client):
@@ -208,17 +211,17 @@ class TestObservationsBlueprint:
208
  "text": "legacy observe call",
209
  "timestamp": _now_iso(),
210
  }
211
- resp = _post_json(client, "/agentmemory/observe", payload)
212
  # The legacy endpoint exists — expect either success or a controlled error, never 404
213
  assert resp.status_code != 404
214
 
215
 
216
  # ===========================================================================
217
  # Blueprint 3: memories.py
218
- # POST /agentmemory/remember
219
- # POST /agentmemory/agent/remember
220
- # GET /agentmemory/memories
221
- # POST /agentmemory/forget
222
  # ===========================================================================
223
 
224
  class TestMemoriesBlueprint:
@@ -229,12 +232,12 @@ class TestMemoriesBlueprint:
229
  "type": "fact",
230
  "concepts": ["sql", "security"],
231
  }
232
- resp = _post_json(client, "/agentmemory/remember", payload)
233
  assert resp.status_code == 201
234
  data = resp.get_json()
235
  assert "memory" in data
236
 
237
- def test_agent_remember_valid_payload(self, client):
238
  """POST /agent/remember with content returns 201."""
239
  payload = {
240
  "content": "The project uses SQLite with WAL mode",
@@ -242,17 +245,17 @@ class TestMemoriesBlueprint:
242
  "type": "architecture",
243
  "concepts": ["sqlite", "wal"],
244
  }
245
- resp = _post_json(client, "/agentmemory/agent/remember", payload)
246
  assert resp.status_code == 201
247
 
248
- def test_agent_remember_missing_content_returns_400(self, client):
249
  """POST /agent/remember without content returns 400."""
250
- resp = _post_json(client, "/agentmemory/agent/remember", {"agentId": "kiro"})
251
  assert resp.status_code == 400
252
 
253
  def test_memories_list_returns_200(self, client):
254
  """GET /memories returns 200 with memories list."""
255
- resp = client.get("/agentmemory/memories")
256
  assert resp.status_code == 200
257
  data = resp.get_json()
258
  assert "memories" in data
@@ -260,7 +263,7 @@ class TestMemoriesBlueprint:
260
 
261
  def test_memories_list_latest_only(self, client):
262
  """GET /memories?latest=true filters to only latest memories."""
263
- resp = client.get("/agentmemory/memories?latest=true")
264
  assert resp.status_code == 200
265
  data = resp.get_json()
266
  # All returned memories should be latest=True or isLatest not False
@@ -270,13 +273,13 @@ class TestMemoriesBlueprint:
270
  def test_forget_memory_by_id(self, client):
271
  """POST /forget with memoryId deletes a global memory."""
272
  # Create a memory first
273
- create_resp = _post_json(client, "/agentmemory/remember", {
274
  "content": "This memory will be forgotten",
275
  "type": "fact",
276
  })
277
  mem_id = create_resp.get_json()["memory"]["id"]
278
  # Forget it
279
- resp = _post_json(client, "/agentmemory/forget", {"memoryId": mem_id})
280
  assert resp.status_code == 200
281
  data = resp.get_json()
282
  assert data["deleted"] >= 1
@@ -285,13 +288,13 @@ class TestMemoriesBlueprint:
285
  """POST /forget with folderPath+agentId clears folder observations."""
286
  # Seed an observation
287
  fp = "/home/user/proj-to-forget"
288
- _post_json(client, "/agentmemory/agent/observe", {
289
  "folderPath": fp,
290
  "agentId": "kiro",
291
  "text": "Some work that will be forgotten",
292
  "timestamp": _now_iso(),
293
  })
294
- resp = _post_json(client, "/agentmemory/forget", {
295
  "folderPath": fp,
296
  "agentId": "kiro",
297
  })
@@ -303,31 +306,31 @@ class TestMemoriesBlueprint:
303
 
304
  # ===========================================================================
305
  # Blueprint 4: search.py
306
- # POST /agentmemory/search
307
- # POST /agentmemory/timeline
308
  # ===========================================================================
309
 
310
  class TestSearchBlueprint:
311
  def test_search_with_query_returns_200(self, client):
312
  """POST /search with a query returns 200."""
313
  # Seed something searchable first
314
- _post_json(client, "/agentmemory/agent/observe", {
315
  "folderPath": "/home/user/proj-search-test",
316
  "agentId": "kiro",
317
  "text": "Implemented authentication middleware for the app",
318
  "timestamp": _now_iso(),
319
  })
320
- resp = _post_json(client, "/agentmemory/search", {"query": "authentication"})
321
  assert resp.status_code == 200
322
 
323
  def test_search_missing_query_returns_400(self, client):
324
  """POST /search without query returns 400."""
325
- resp = _post_json(client, "/agentmemory/search", {})
326
  assert resp.status_code == 400
327
 
328
  def test_search_with_folder_filter(self, client):
329
  """POST /search with folderPath filter returns 200."""
330
- resp = _post_json(client, "/agentmemory/search", {
331
  "query": "test",
332
  "folderPath": "/home/user/proj-search-test",
333
  "agentId": "kiro",
@@ -336,7 +339,7 @@ class TestSearchBlueprint:
336
 
337
  def test_timeline_returns_200(self, client):
338
  """POST /timeline returns 200 with observations list."""
339
- resp = _post_json(client, "/agentmemory/timeline", {})
340
  assert resp.status_code == 200
341
  data = resp.get_json()
342
  assert "observations" in data
@@ -344,7 +347,7 @@ class TestSearchBlueprint:
344
 
345
  def test_timeline_with_filters_returns_200(self, client):
346
  """POST /timeline with folder/agent filters returns 200."""
347
- resp = _post_json(client, "/agentmemory/timeline", {
348
  "folderPath": "/home/user/proj-search-test",
349
  "agentId": "kiro",
350
  "limit": 10,
@@ -357,11 +360,11 @@ class TestSearchBlueprint:
357
  # Seed observations with distinct timestamps
358
  for i in range(3):
359
  ts = datetime.datetime(2025, 6, 1, 10, i, 0).isoformat() + "Z"
360
- _post_json(client, "/agentmemory/agent/observe", {
361
  "folderPath": fp, "agentId": "kiro",
362
  "text": f"Observation {i}", "timestamp": ts,
363
  })
364
- resp = _post_json(client, "/agentmemory/timeline", {
365
  "folderPath": fp, "agentId": "kiro",
366
  })
367
  assert resp.status_code == 200
@@ -372,16 +375,16 @@ class TestSearchBlueprint:
372
 
373
  # ===========================================================================
374
  # Blueprint 5: graph.py
375
- # GET /agentmemory/graph
376
- # GET /agentmemory/graph/stats
377
- # POST /agentmemory/graph/query
378
- # POST /agentmemory/graph/build
379
  # ===========================================================================
380
 
381
  class TestGraphBlueprint:
382
  def test_graph_returns_200(self, client):
383
  """GET /graph returns 200 with nodes and edges."""
384
- resp = client.get("/agentmemory/graph")
385
  assert resp.status_code == 200
386
  data = resp.get_json()
387
  assert "nodes" in data
@@ -389,7 +392,7 @@ class TestGraphBlueprint:
389
 
390
  def test_graph_stats_returns_200(self, client):
391
  """GET /graph/stats returns 200."""
392
- resp = client.get("/agentmemory/graph/stats")
393
  assert resp.status_code == 200
394
  data = resp.get_json()
395
  assert "nodes" in data
@@ -397,24 +400,24 @@ class TestGraphBlueprint:
397
 
398
  def test_graph_query_returns_200(self, client):
399
  """POST /graph/query returns 200."""
400
- resp = _post_json(client, "/agentmemory/graph/query", {})
401
  assert resp.status_code == 200
402
 
403
  def test_graph_build_returns_200(self, client):
404
  """POST /graph/build returns 200."""
405
- resp = _post_json(client, "/agentmemory/graph/build", {})
406
  assert resp.status_code == 200
407
 
408
  def test_graph_nodes_have_required_fields(self, client):
409
  """Graph nodes contain id, label, folderPath, agentIds, obsCount, color (REQ-024)."""
410
  # Seed some data
411
- _post_json(client, "/agentmemory/agent/observe", {
412
  "folderPath": "/home/user/proj-graph-check",
413
  "agentId": "kiro",
414
  "text": "Graph node field check",
415
  "timestamp": _now_iso(),
416
  })
417
- resp = client.get("/agentmemory/graph")
418
  data = resp.get_json()
419
  if data["nodes"]:
420
  node = data["nodes"][0]
@@ -424,14 +427,14 @@ class TestGraphBlueprint:
424
 
425
  # ===========================================================================
426
  # Blueprint 6: mcp.py
427
- # GET /agentmemory/mcp/tools
428
- # POST /agentmemory/mcp/tools
429
  # ===========================================================================
430
 
431
  class TestMcpBlueprint:
432
  def test_mcp_tools_list_returns_200(self, client):
433
  """GET /mcp/tools returns 200 with a tools list."""
434
- resp = client.get("/agentmemory/mcp/tools")
435
  assert resp.status_code == 200
436
  data = resp.get_json()
437
  assert "tools" in data
@@ -440,20 +443,20 @@ class TestMcpBlueprint:
440
 
441
  def test_mcp_tools_contains_required_tools(self, client):
442
  """The expected MCP tools are all present in the schema."""
443
- resp = client.get("/agentmemory/mcp/tools")
444
  tool_names = {t["name"] for t in resp.get_json()["tools"]}
445
  required_tools = {
446
  "agent_observe",
447
- "memory_recall",
448
- "memory_save",
449
- "agent_remember",
450
- "memory_diagnose",
451
- "memory_forget",
452
- "memory_export",
453
- "memory_smart_search",
454
- "memory_folders",
455
- "memory_folder_observations",
456
- "memory_timeline",
457
  }
458
  assert required_tools.issubset(tool_names), \
459
  f"Missing tools: {required_tools - tool_names}"
@@ -469,23 +472,23 @@ class TestMcpBlueprint:
469
  "timestamp": _now_iso(),
470
  },
471
  }
472
- resp = _post_json(client, "/agentmemory/mcp/tools", payload)
473
  assert resp.status_code == 200
474
  data = resp.get_json()
475
  assert "content" in data
476
 
477
- def test_mcp_tool_call_memory_recall(self, client):
478
- """POST /mcp/tools memory_recall returns 200."""
479
- resp = _post_json(client, "/agentmemory/mcp/tools", {
480
- "name": "memory_recall",
481
  "arguments": {"query": "authentication"},
482
  })
483
  assert resp.status_code == 200
484
 
485
- def test_mcp_tool_call_memory_diagnose(self, client):
486
- """POST /mcp/tools memory_diagnose returns folderCount etc."""
487
- resp = _post_json(client, "/agentmemory/mcp/tools", {
488
- "name": "memory_diagnose",
489
  "arguments": {},
490
  })
491
  assert resp.status_code == 200
@@ -494,25 +497,25 @@ class TestMcpBlueprint:
494
  assert "folderCount" in result
495
  assert "observationCount" in result
496
 
497
- def test_mcp_tool_call_memory_folders(self, client):
498
- """POST /mcp/tools memory_folders returns list."""
499
- resp = _post_json(client, "/agentmemory/mcp/tools", {
500
- "name": "memory_folders",
501
  "arguments": {},
502
  })
503
  assert resp.status_code == 200
504
 
505
- def test_mcp_tool_call_memory_timeline(self, client):
506
- """POST /mcp/tools memory_timeline returns list."""
507
- resp = _post_json(client, "/agentmemory/mcp/tools", {
508
- "name": "memory_timeline",
509
  "arguments": {"limit": 10},
510
  })
511
  assert resp.status_code == 200
512
 
513
  def test_mcp_tool_unknown_name_returns_400(self, client):
514
  """POST /mcp/tools with unknown tool name returns 400."""
515
- resp = _post_json(client, "/agentmemory/mcp/tools", {
516
  "name": "nonexistent_tool_xyz",
517
  "arguments": {},
518
  })
@@ -520,19 +523,19 @@ class TestMcpBlueprint:
520
 
521
  def test_mcp_tool_missing_name_returns_400(self, client):
522
  """POST /mcp/tools without name returns 400."""
523
- resp = _post_json(client, "/agentmemory/mcp/tools", {"arguments": {}})
524
  assert resp.status_code == 400
525
 
526
 
527
  # ===========================================================================
528
  # Blueprint 7: migration.py
529
- # POST /agentmemory/migrate
530
  # ===========================================================================
531
 
532
  class TestMigrationBlueprint:
533
  def test_migrate_dry_run_returns_200(self, client):
534
  """POST /migrate with dry_run=true returns 200 with counts."""
535
- resp = _post_json(client, "/agentmemory/migrate", {"dry_run": True})
536
  assert resp.status_code == 200
537
  data = resp.get_json()
538
  assert "migrated_sessions" in data
@@ -541,7 +544,7 @@ class TestMigrationBlueprint:
541
 
542
  def test_migrate_without_body_defaults_to_no_dry_run(self, client):
543
  """POST /migrate with empty body returns 200."""
544
- resp = _post_json(client, "/agentmemory/migrate", {})
545
  assert resp.status_code == 200
546
 
547
 
@@ -555,48 +558,48 @@ class TestBlueprintRegistration:
555
  registered_rules = {rule.rule for rule in flask_app.url_map.iter_rules()}
556
 
557
  # observations.py
558
- assert "/agentmemory/agent/observe" in registered_rules
559
- assert "/agentmemory/folders" in registered_rules
560
- assert "/agentmemory/folder/observations" in registered_rules
561
 
562
  # memories.py
563
- assert "/agentmemory/remember" in registered_rules
564
- assert "/agentmemory/memories" in registered_rules
565
- assert "/agentmemory/forget" in registered_rules
566
 
567
  # search.py
568
- assert "/agentmemory/search" in registered_rules
569
- assert "/agentmemory/timeline" in registered_rules
570
 
571
  # graph.py
572
- assert "/agentmemory/graph" in registered_rules
573
- assert "/agentmemory/graph/stats" in registered_rules
574
- assert "/agentmemory/graph/query" in registered_rules
575
 
576
  # health.py
577
- assert "/agentmemory/livez" in registered_rules
578
- assert "/agentmemory/health" in registered_rules
579
- assert "/agentmemory/audit" in registered_rules
580
- assert "/agentmemory/config/flags" in registered_rules
581
 
582
  # mcp.py
583
- assert "/agentmemory/mcp/tools" in registered_rules
584
 
585
  # migration.py
586
- assert "/agentmemory/migrate" in registered_rules
587
 
588
  def test_no_routes_return_404_from_blueprints(self, client):
589
  """None of the expected blueprint routes return 404."""
590
  get_routes = [
591
- "/agentmemory/livez",
592
- "/agentmemory/health",
593
- "/agentmemory/folders",
594
- "/agentmemory/memories",
595
- "/agentmemory/graph",
596
- "/agentmemory/graph/stats",
597
- "/agentmemory/audit",
598
- "/agentmemory/config/flags",
599
- "/agentmemory/mcp/tools",
600
  ]
601
  for route in get_routes:
602
  resp = client.get(route)
@@ -605,7 +608,7 @@ class TestBlueprintRegistration:
605
  def test_cors_headers_present_on_responses(self, client):
606
  """CORS after_request hook adds the expected headers."""
607
  resp = client.get(
608
- "/agentmemory/livez",
609
  headers={"Origin": "http://localhost:3000"},
610
  )
611
  assert resp.status_code == 200
@@ -614,35 +617,35 @@ class TestBlueprintRegistration:
614
  assert "Access-Control-Allow-Methods" in resp.headers
615
 
616
  def test_auth_returns_401_when_secret_set(self, client, flask_app):
617
- """When AGENTMEMORY_SECRET is configured, missing token returns 401 on protected routes.
618
 
619
- /agentmemory/audit requires auth; /agentmemory/livez and /agentmemory/health are open.
620
  """
621
- os.environ["AGENTMEMORY_SECRET"] = "test-secret-token"
622
  try:
623
- resp = client.get("/agentmemory/audit")
624
  assert resp.status_code == 401
625
  finally:
626
- del os.environ["AGENTMEMORY_SECRET"]
627
 
628
  def test_auth_passes_with_correct_bearer_token(self, client, flask_app):
629
  """Correct Bearer token passes auth on protected endpoints."""
630
  secret = "test-secret-token-correct"
631
- os.environ["AGENTMEMORY_SECRET"] = secret
632
  try:
633
  resp = client.get(
634
- "/agentmemory/audit",
635
  headers={"Authorization": f"Bearer {secret}"},
636
  )
637
  assert resp.status_code == 200
638
  finally:
639
- del os.environ["AGENTMEMORY_SECRET"]
640
 
641
  def test_livez_always_open_regardless_of_secret(self, client):
642
  """GET /livez is always unauthenticated, even when secret is set (REQ-057)."""
643
- os.environ["AGENTMEMORY_SECRET"] = "some-secret"
644
  try:
645
- resp = client.get("/agentmemory/livez")
646
  assert resp.status_code == 200
647
  finally:
648
- del os.environ["AGENTMEMORY_SECRET"]
 
6
  src/routes/{observations, memories, search, graph, health, mcp, migration}.py.
7
 
8
  Tests use the Flask test client — no running server required.
9
+ The AGENTCACHE_SECRET env var is NOT set so all auth checks pass through.
10
  """
11
  import sys
12
  import os
 
36
  db_path = str(tmp_dir / "test.db")
37
 
38
  # Point the server at an isolated DB
39
+ os.environ["AGENTCACHE_DB_PATH"] = db_path
40
  # Ensure no auth requirement during tests
41
+ os.environ.pop("AGENTCACHE_SECRET", None)
42
  os.environ.pop("AGENTMEMORY_SECRET", None)
43
 
44
  import app as app_module
45
+ os.environ.pop("AGENTCACHE_SECRET", None)
46
+ os.environ.pop("AGENTMEMORY_SECRET", None)
47
  from db import StateKV
48
 
49
  # Patch StateKV to use tmp db before create_app() initialises it
 
84
 
85
  # ===========================================================================
86
  # Blueprint 1: health.py
87
+ # GET /agentcache/livez
88
+ # GET /agentcache/health
89
+ # GET /agentcache/audit
90
+ # GET /agentcache/config/flags
91
  # ===========================================================================
92
 
93
  class TestHealthBlueprint:
94
  def test_livez_no_auth_required(self, client):
95
  """GET /livez must respond 200 without any auth token (always open)."""
96
+ resp = client.get("/agentcache/livez")
97
  assert resp.status_code == 200
98
  data = resp.get_json()
99
  assert data["status"] == "ok"
100
  assert "service" in data
101
 
102
  def test_livez_returns_service_name(self, client):
103
+ resp = client.get("/agentcache/livez")
104
  data = resp.get_json()
105
+ assert data["service"] == "agentcache"
106
 
107
  def test_health_returns_200(self, client):
108
+ resp = client.get("/agentcache/health")
109
  assert resp.status_code == 200
110
  data = resp.get_json()
111
  # Folder-based health check fields (REQ-047)
 
114
  assert "memoryCount" in data
115
 
116
  def test_audit_returns_200(self, client):
117
+ resp = client.get("/agentcache/audit")
118
  assert resp.status_code == 200
119
  data = resp.get_json()
120
  assert "entries" in data
121
 
122
  def test_config_flags_returns_200(self, client):
123
+ resp = client.get("/agentcache/config/flags")
124
  assert resp.status_code == 200
125
  data = resp.get_json()
126
  assert "flags" in data
 
129
 
130
  # ===========================================================================
131
  # Blueprint 2: observations.py
132
+ # POST /agentcache/observe
133
+ # POST /agentcache/agent/observe
134
+ # GET /agentcache/folders
135
+ # GET /agentcache/folder/observations
136
  # ===========================================================================
137
 
138
  class TestObservationsBlueprint:
 
144
  "text": "Added a new feature to the app",
145
  "timestamp": _now_iso(),
146
  }
147
+ resp = _post_json(client, "/agentcache/agent/observe", payload)
148
  assert resp.status_code == 201
149
  data = resp.get_json()
150
  assert "observationId" in data
 
153
  def test_agent_observe_missing_folder_path_returns_400(self, client):
154
  """POST /agent/observe missing folderPath returns 400."""
155
  payload = {"agentId": "kiro", "text": "some work", "timestamp": _now_iso()}
156
+ resp = _post_json(client, "/agentcache/agent/observe", payload)
157
  assert resp.status_code == 400
158
 
159
  def test_agent_observe_missing_agent_id_returns_400(self, client):
160
  """POST /agent/observe missing agentId returns 400."""
161
  payload = {"folderPath": "/home/user/proj", "text": "some work", "timestamp": _now_iso()}
162
+ resp = _post_json(client, "/agentcache/agent/observe", payload)
163
  assert resp.status_code == 400
164
 
165
  def test_agent_observe_missing_text_returns_400(self, client):
166
  """POST /agent/observe missing text returns 400."""
167
  payload = {"folderPath": "/home/user/proj", "agentId": "kiro", "timestamp": _now_iso()}
168
+ resp = _post_json(client, "/agentcache/agent/observe", payload)
169
  assert resp.status_code == 400
170
 
171
  def test_folders_list_returns_200(self, client):
172
  """GET /folders returns 200 with a folders list."""
173
  # Seed at least one observation first
174
+ _post_json(client, "/agentcache/agent/observe", {
175
  "folderPath": "/home/user/proj-folders-test",
176
  "agentId": "kiro",
177
  "text": "Folders test observation",
178
  "timestamp": _now_iso(),
179
  })
180
+ resp = client.get("/agentcache/folders")
181
  assert resp.status_code == 200
182
  data = resp.get_json()
183
  assert "folders" in data
 
187
  """GET /folder/observations with valid params returns 200."""
188
  # Seed an observation first
189
  fp = "/home/user/proj-obs-test"
190
+ _post_json(client, "/agentcache/agent/observe", {
191
  "folderPath": fp,
192
  "agentId": "kiro",
193
  "text": "Obs for folder/observations test",
194
  "timestamp": _now_iso(),
195
  })
196
+ resp = client.get("/agentcache/folder/observations?folderPath=home/user/proj-obs-test&agentId=kiro")
197
  assert resp.status_code == 200
198
  data = resp.get_json()
199
  assert "observations" in data
200
 
201
  def test_folder_observations_missing_params_returns_400(self, client):
202
  """GET /folder/observations without required params returns 400."""
203
+ resp = client.get("/agentcache/folder/observations")
204
  assert resp.status_code == 400
205
 
206
  def test_legacy_observe_endpoint_returns_400_or_201(self, client):
 
211
  "text": "legacy observe call",
212
  "timestamp": _now_iso(),
213
  }
214
+ resp = _post_json(client, "/agentcache/observe", payload)
215
  # The legacy endpoint exists — expect either success or a controlled error, never 404
216
  assert resp.status_code != 404
217
 
218
 
219
  # ===========================================================================
220
  # Blueprint 3: memories.py
221
+ # POST /agentcache/remember
222
+ # POST /agentcache/agent/remember
223
+ # GET /agentcache/memories
224
+ # POST /agentcache/forget
225
  # ===========================================================================
226
 
227
  class TestMemoriesBlueprint:
 
232
  "type": "fact",
233
  "concepts": ["sql", "security"],
234
  }
235
+ resp = _post_json(client, "/agentcache/remember", payload)
236
  assert resp.status_code == 201
237
  data = resp.get_json()
238
  assert "memory" in data
239
 
240
+ def test_agent_cache_valid_payload(self, client):
241
  """POST /agent/remember with content returns 201."""
242
  payload = {
243
  "content": "The project uses SQLite with WAL mode",
 
245
  "type": "architecture",
246
  "concepts": ["sqlite", "wal"],
247
  }
248
+ resp = _post_json(client, "/agentcache/agent/remember", payload)
249
  assert resp.status_code == 201
250
 
251
+ def test_agent_cache_missing_content_returns_400(self, client):
252
  """POST /agent/remember without content returns 400."""
253
+ resp = _post_json(client, "/agentcache/agent/remember", {"agentId": "kiro"})
254
  assert resp.status_code == 400
255
 
256
  def test_memories_list_returns_200(self, client):
257
  """GET /memories returns 200 with memories list."""
258
+ resp = client.get("/agentcache/memories")
259
  assert resp.status_code == 200
260
  data = resp.get_json()
261
  assert "memories" in data
 
263
 
264
  def test_memories_list_latest_only(self, client):
265
  """GET /memories?latest=true filters to only latest memories."""
266
+ resp = client.get("/agentcache/memories?latest=true")
267
  assert resp.status_code == 200
268
  data = resp.get_json()
269
  # All returned memories should be latest=True or isLatest not False
 
273
  def test_forget_memory_by_id(self, client):
274
  """POST /forget with memoryId deletes a global memory."""
275
  # Create a memory first
276
+ create_resp = _post_json(client, "/agentcache/remember", {
277
  "content": "This memory will be forgotten",
278
  "type": "fact",
279
  })
280
  mem_id = create_resp.get_json()["memory"]["id"]
281
  # Forget it
282
+ resp = _post_json(client, "/agentcache/forget", {"memoryId": mem_id})
283
  assert resp.status_code == 200
284
  data = resp.get_json()
285
  assert data["deleted"] >= 1
 
288
  """POST /forget with folderPath+agentId clears folder observations."""
289
  # Seed an observation
290
  fp = "/home/user/proj-to-forget"
291
+ _post_json(client, "/agentcache/agent/observe", {
292
  "folderPath": fp,
293
  "agentId": "kiro",
294
  "text": "Some work that will be forgotten",
295
  "timestamp": _now_iso(),
296
  })
297
+ resp = _post_json(client, "/agentcache/forget", {
298
  "folderPath": fp,
299
  "agentId": "kiro",
300
  })
 
306
 
307
  # ===========================================================================
308
  # Blueprint 4: search.py
309
+ # POST /agentcache/search
310
+ # POST /agentcache/timeline
311
  # ===========================================================================
312
 
313
  class TestSearchBlueprint:
314
  def test_search_with_query_returns_200(self, client):
315
  """POST /search with a query returns 200."""
316
  # Seed something searchable first
317
+ _post_json(client, "/agentcache/agent/observe", {
318
  "folderPath": "/home/user/proj-search-test",
319
  "agentId": "kiro",
320
  "text": "Implemented authentication middleware for the app",
321
  "timestamp": _now_iso(),
322
  })
323
+ resp = _post_json(client, "/agentcache/search", {"query": "authentication"})
324
  assert resp.status_code == 200
325
 
326
  def test_search_missing_query_returns_400(self, client):
327
  """POST /search without query returns 400."""
328
+ resp = _post_json(client, "/agentcache/search", {})
329
  assert resp.status_code == 400
330
 
331
  def test_search_with_folder_filter(self, client):
332
  """POST /search with folderPath filter returns 200."""
333
+ resp = _post_json(client, "/agentcache/search", {
334
  "query": "test",
335
  "folderPath": "/home/user/proj-search-test",
336
  "agentId": "kiro",
 
339
 
340
  def test_timeline_returns_200(self, client):
341
  """POST /timeline returns 200 with observations list."""
342
+ resp = _post_json(client, "/agentcache/timeline", {})
343
  assert resp.status_code == 200
344
  data = resp.get_json()
345
  assert "observations" in data
 
347
 
348
  def test_timeline_with_filters_returns_200(self, client):
349
  """POST /timeline with folder/agent filters returns 200."""
350
+ resp = _post_json(client, "/agentcache/timeline", {
351
  "folderPath": "/home/user/proj-search-test",
352
  "agentId": "kiro",
353
  "limit": 10,
 
360
  # Seed observations with distinct timestamps
361
  for i in range(3):
362
  ts = datetime.datetime(2025, 6, 1, 10, i, 0).isoformat() + "Z"
363
+ _post_json(client, "/agentcache/agent/observe", {
364
  "folderPath": fp, "agentId": "kiro",
365
  "text": f"Observation {i}", "timestamp": ts,
366
  })
367
+ resp = _post_json(client, "/agentcache/timeline", {
368
  "folderPath": fp, "agentId": "kiro",
369
  })
370
  assert resp.status_code == 200
 
375
 
376
  # ===========================================================================
377
  # Blueprint 5: graph.py
378
+ # GET /agentcache/graph
379
+ # GET /agentcache/graph/stats
380
+ # POST /agentcache/graph/query
381
+ # POST /agentcache/graph/build
382
  # ===========================================================================
383
 
384
  class TestGraphBlueprint:
385
  def test_graph_returns_200(self, client):
386
  """GET /graph returns 200 with nodes and edges."""
387
+ resp = client.get("/agentcache/graph")
388
  assert resp.status_code == 200
389
  data = resp.get_json()
390
  assert "nodes" in data
 
392
 
393
  def test_graph_stats_returns_200(self, client):
394
  """GET /graph/stats returns 200."""
395
+ resp = client.get("/agentcache/graph/stats")
396
  assert resp.status_code == 200
397
  data = resp.get_json()
398
  assert "nodes" in data
 
400
 
401
  def test_graph_query_returns_200(self, client):
402
  """POST /graph/query returns 200."""
403
+ resp = _post_json(client, "/agentcache/graph/query", {})
404
  assert resp.status_code == 200
405
 
406
  def test_graph_build_returns_200(self, client):
407
  """POST /graph/build returns 200."""
408
+ resp = _post_json(client, "/agentcache/graph/build", {})
409
  assert resp.status_code == 200
410
 
411
  def test_graph_nodes_have_required_fields(self, client):
412
  """Graph nodes contain id, label, folderPath, agentIds, obsCount, color (REQ-024)."""
413
  # Seed some data
414
+ _post_json(client, "/agentcache/agent/observe", {
415
  "folderPath": "/home/user/proj-graph-check",
416
  "agentId": "kiro",
417
  "text": "Graph node field check",
418
  "timestamp": _now_iso(),
419
  })
420
+ resp = client.get("/agentcache/graph")
421
  data = resp.get_json()
422
  if data["nodes"]:
423
  node = data["nodes"][0]
 
427
 
428
  # ===========================================================================
429
  # Blueprint 6: mcp.py
430
+ # GET /agentcache/mcp/tools
431
+ # POST /agentcache/mcp/tools
432
  # ===========================================================================
433
 
434
  class TestMcpBlueprint:
435
  def test_mcp_tools_list_returns_200(self, client):
436
  """GET /mcp/tools returns 200 with a tools list."""
437
+ resp = client.get("/agentcache/mcp/tools")
438
  assert resp.status_code == 200
439
  data = resp.get_json()
440
  assert "tools" in data
 
443
 
444
  def test_mcp_tools_contains_required_tools(self, client):
445
  """The expected MCP tools are all present in the schema."""
446
+ resp = client.get("/agentcache/mcp/tools")
447
  tool_names = {t["name"] for t in resp.get_json()["tools"]}
448
  required_tools = {
449
  "agent_observe",
450
+ "cache_recall",
451
+ "cache_save",
452
+ "agent_cache",
453
+ "cache_diagnose",
454
+ "cache_forget",
455
+ "cache_export",
456
+ "cache_smart_search",
457
+ "cache_folders",
458
+ "cache_folder_observations",
459
+ "cache_timeline",
460
  }
461
  assert required_tools.issubset(tool_names), \
462
  f"Missing tools: {required_tools - tool_names}"
 
472
  "timestamp": _now_iso(),
473
  },
474
  }
475
+ resp = _post_json(client, "/agentcache/mcp/tools", payload)
476
  assert resp.status_code == 200
477
  data = resp.get_json()
478
  assert "content" in data
479
 
480
+ def test_mcp_tool_call_cache_recall(self, client):
481
+ """POST /mcp/tools cache_recall returns 200."""
482
+ resp = _post_json(client, "/agentcache/mcp/tools", {
483
+ "name": "cache_recall",
484
  "arguments": {"query": "authentication"},
485
  })
486
  assert resp.status_code == 200
487
 
488
+ def test_mcp_tool_call_cache_diagnose(self, client):
489
+ """POST /mcp/tools cache_diagnose returns folderCount etc."""
490
+ resp = _post_json(client, "/agentcache/mcp/tools", {
491
+ "name": "cache_diagnose",
492
  "arguments": {},
493
  })
494
  assert resp.status_code == 200
 
497
  assert "folderCount" in result
498
  assert "observationCount" in result
499
 
500
+ def test_mcp_tool_call_cache_folders(self, client):
501
+ """POST /mcp/tools cache_folders returns list."""
502
+ resp = _post_json(client, "/agentcache/mcp/tools", {
503
+ "name": "cache_folders",
504
  "arguments": {},
505
  })
506
  assert resp.status_code == 200
507
 
508
+ def test_mcp_tool_call_cache_timeline(self, client):
509
+ """POST /mcp/tools cache_timeline returns list."""
510
+ resp = _post_json(client, "/agentcache/mcp/tools", {
511
+ "name": "cache_timeline",
512
  "arguments": {"limit": 10},
513
  })
514
  assert resp.status_code == 200
515
 
516
  def test_mcp_tool_unknown_name_returns_400(self, client):
517
  """POST /mcp/tools with unknown tool name returns 400."""
518
+ resp = _post_json(client, "/agentcache/mcp/tools", {
519
  "name": "nonexistent_tool_xyz",
520
  "arguments": {},
521
  })
 
523
 
524
  def test_mcp_tool_missing_name_returns_400(self, client):
525
  """POST /mcp/tools without name returns 400."""
526
+ resp = _post_json(client, "/agentcache/mcp/tools", {"arguments": {}})
527
  assert resp.status_code == 400
528
 
529
 
530
  # ===========================================================================
531
  # Blueprint 7: migration.py
532
+ # POST /agentcache/migrate
533
  # ===========================================================================
534
 
535
  class TestMigrationBlueprint:
536
  def test_migrate_dry_run_returns_200(self, client):
537
  """POST /migrate with dry_run=true returns 200 with counts."""
538
+ resp = _post_json(client, "/agentcache/migrate", {"dry_run": True})
539
  assert resp.status_code == 200
540
  data = resp.get_json()
541
  assert "migrated_sessions" in data
 
544
 
545
  def test_migrate_without_body_defaults_to_no_dry_run(self, client):
546
  """POST /migrate with empty body returns 200."""
547
+ resp = _post_json(client, "/agentcache/migrate", {})
548
  assert resp.status_code == 200
549
 
550
 
 
558
  registered_rules = {rule.rule for rule in flask_app.url_map.iter_rules()}
559
 
560
  # observations.py
561
+ assert "/agentcache/agent/observe" in registered_rules
562
+ assert "/agentcache/folders" in registered_rules
563
+ assert "/agentcache/folder/observations" in registered_rules
564
 
565
  # memories.py
566
+ assert "/agentcache/remember" in registered_rules
567
+ assert "/agentcache/memories" in registered_rules
568
+ assert "/agentcache/forget" in registered_rules
569
 
570
  # search.py
571
+ assert "/agentcache/search" in registered_rules
572
+ assert "/agentcache/timeline" in registered_rules
573
 
574
  # graph.py
575
+ assert "/agentcache/graph" in registered_rules
576
+ assert "/agentcache/graph/stats" in registered_rules
577
+ assert "/agentcache/graph/query" in registered_rules
578
 
579
  # health.py
580
+ assert "/agentcache/livez" in registered_rules
581
+ assert "/agentcache/health" in registered_rules
582
+ assert "/agentcache/audit" in registered_rules
583
+ assert "/agentcache/config/flags" in registered_rules
584
 
585
  # mcp.py
586
+ assert "/agentcache/mcp/tools" in registered_rules
587
 
588
  # migration.py
589
+ assert "/agentcache/migrate" in registered_rules
590
 
591
  def test_no_routes_return_404_from_blueprints(self, client):
592
  """None of the expected blueprint routes return 404."""
593
  get_routes = [
594
+ "/agentcache/livez",
595
+ "/agentcache/health",
596
+ "/agentcache/folders",
597
+ "/agentcache/memories",
598
+ "/agentcache/graph",
599
+ "/agentcache/graph/stats",
600
+ "/agentcache/audit",
601
+ "/agentcache/config/flags",
602
+ "/agentcache/mcp/tools",
603
  ]
604
  for route in get_routes:
605
  resp = client.get(route)
 
608
  def test_cors_headers_present_on_responses(self, client):
609
  """CORS after_request hook adds the expected headers."""
610
  resp = client.get(
611
+ "/agentcache/livez",
612
  headers={"Origin": "http://localhost:3000"},
613
  )
614
  assert resp.status_code == 200
 
617
  assert "Access-Control-Allow-Methods" in resp.headers
618
 
619
  def test_auth_returns_401_when_secret_set(self, client, flask_app):
620
+ """When AGENTCACHE_SECRET is configured, missing token returns 401 on protected routes.
621
 
622
+ /agentcache/audit requires auth; /agentcache/livez and /agentcache/health are open.
623
  """
624
+ os.environ["AGENTCACHE_SECRET"] = "test-secret-token"
625
  try:
626
+ resp = client.get("/agentcache/audit")
627
  assert resp.status_code == 401
628
  finally:
629
+ del os.environ["AGENTCACHE_SECRET"]
630
 
631
  def test_auth_passes_with_correct_bearer_token(self, client, flask_app):
632
  """Correct Bearer token passes auth on protected endpoints."""
633
  secret = "test-secret-token-correct"
634
+ os.environ["AGENTCACHE_SECRET"] = secret
635
  try:
636
  resp = client.get(
637
+ "/agentcache/audit",
638
  headers={"Authorization": f"Bearer {secret}"},
639
  )
640
  assert resp.status_code == 200
641
  finally:
642
+ del os.environ["AGENTCACHE_SECRET"]
643
 
644
  def test_livez_always_open_regardless_of_secret(self, client):
645
  """GET /livez is always unauthenticated, even when secret is set (REQ-057)."""
646
+ os.environ["AGENTCACHE_SECRET"] = "some-secret"
647
  try:
648
+ resp = client.get("/agentcache/livez")
649
  assert resp.status_code == 200
650
  finally:
651
+ del os.environ["AGENTCACHE_SECRET"]
tests/test_timeline.py CHANGED
@@ -48,14 +48,14 @@ class TestTimelineLimit:
48
  def test_limit_respected(self, tmp_path):
49
  kv = make_kv(tmp_path)
50
  for i in range(10):
51
- add_obs(kv, timestamp=ts(i))
52
  results = folder_timeline(kv, limit=5)
53
  assert len(results) == 5
54
 
55
  def test_default_limit_100(self, tmp_path):
56
  kv = make_kv(tmp_path)
57
  for i in range(150):
58
- add_obs(kv, timestamp=ts(i))
59
  results = folder_timeline(kv)
60
  assert len(results) == 100
61
 
 
48
  def test_limit_respected(self, tmp_path):
49
  kv = make_kv(tmp_path)
50
  for i in range(10):
51
+ add_obs(kv, timestamp=ts(i), text=f"obs {i}")
52
  results = folder_timeline(kv, limit=5)
53
  assert len(results) == 5
54
 
55
  def test_default_limit_100(self, tmp_path):
56
  kv = make_kv(tmp_path)
57
  for i in range(150):
58
+ add_obs(kv, timestamp=ts(i), text=f"obs {i}")
59
  results = folder_timeline(kv)
60
  assert len(results) == 100
61