diff --git a/agent/memory/service.py b/agent/memory/service.py
index 567a1f5f..eb565b13 100644
--- a/agent/memory/service.py
+++ b/agent/memory/service.py
@@ -32,68 +32,80 @@ class MemoryService:
# ------------------------------------------------------------------
# list — paginated file metadata
# ------------------------------------------------------------------
- def list_files(self, page: int = 1, page_size: int = 20) -> dict:
+ def list_files(self, page: int = 1, page_size: int = 20, category: str = "memory") -> dict:
"""
- List all memory files with metadata (without content).
+ List memory or dream files with metadata (without content).
- Returns::
-
- {
- "page": 1,
- "page_size": 20,
- "total": 15,
- "list": [
- {"filename": "MEMORY.md", "type": "global", "size": 2048, "updated_at": "2026-02-20 10:00:00"},
- {"filename": "2026-02-20.md", "type": "daily", "size": 512, "updated_at": "2026-02-20 09:30:00"},
- ...
- ]
- }
+ Args:
+ category: ``"memory"`` (default) — MEMORY.md + daily files;
+ ``"dream"`` — dream diary files from memory/dreams/
"""
+ if category == "dream":
+ files = self._list_dream_files()
+ else:
+ files = self._list_memory_files()
+
+ total = len(files)
+ start = (page - 1) * page_size
+ end = start + page_size
+
+ return {
+ "page": page,
+ "page_size": page_size,
+ "total": total,
+ "list": files[start:end],
+ }
+
+ def _list_memory_files(self) -> List[dict]:
+ """MEMORY.md + memory/*.md (newest first)."""
files: List[dict] = []
- # 1. Global memory — MEMORY.md in workspace root
global_path = os.path.join(self.workspace_root, "MEMORY.md")
if os.path.isfile(global_path):
files.append(self._file_info(global_path, "MEMORY.md", "global"))
- # 2. Daily memory files — memory/*.md (sorted newest first)
if os.path.isdir(self.memory_dir):
daily_files = []
for name in os.listdir(self.memory_dir):
full = os.path.join(self.memory_dir, name)
if os.path.isfile(full) and name.endswith(".md"):
daily_files.append((name, full))
- # Sort by filename descending (newest date first)
daily_files.sort(key=lambda x: x[0], reverse=True)
for name, full in daily_files:
files.append(self._file_info(full, name, "daily"))
- total = len(files)
+ return files
- # Paginate
- start = (page - 1) * page_size
- end = start + page_size
- page_items = files[start:end]
+ def _list_dream_files(self) -> List[dict]:
+ """memory/dreams/*.md (newest first)."""
+ files: List[dict] = []
+ dreams_dir = os.path.join(self.memory_dir, "dreams")
- return {
- "page": page,
- "page_size": page_size,
- "total": total,
- "list": page_items,
- }
+ if os.path.isdir(dreams_dir):
+ entries = []
+ for name in os.listdir(dreams_dir):
+ full = os.path.join(dreams_dir, name)
+ if os.path.isfile(full) and name.endswith(".md"):
+ entries.append((name, full))
+ entries.sort(key=lambda x: x[0], reverse=True)
+ for name, full in entries:
+ files.append(self._file_info(full, name, "dream"))
+
+ return files
# ------------------------------------------------------------------
# content — read a single file
# ------------------------------------------------------------------
- def get_content(self, filename: str) -> dict:
+ def get_content(self, filename: str, category: str = "memory") -> dict:
"""
- Read the full content of a memory file.
+ Read the full content of a memory or dream file.
- :param filename: File name, e.g. ``MEMORY.md`` or ``2026-02-20.md``
+ :param filename: File name, e.g. ``MEMORY.md``, ``2026-02-20.md``
+ :param category: ``"memory"`` or ``"dream"``
:return: dict with ``filename`` and ``content``
:raises FileNotFoundError: if the file does not exist
"""
- path = self._resolve_path(filename)
+ path = self._resolve_path(filename, category)
if not os.path.isfile(path):
raise FileNotFoundError(f"Memory file not found: {filename}")
@@ -113,7 +125,7 @@ class MemoryService:
Dispatch a memory management action.
:param action: ``list`` or ``content``
- :param payload: action-specific payload
+ :param payload: action-specific payload (supports ``category``: ``"memory"`` | ``"dream"``)
:return: protocol-compatible response dict
"""
payload = payload or {}
@@ -121,14 +133,16 @@ class MemoryService:
if action == "list":
page = payload.get("page", 1)
page_size = payload.get("page_size", 20)
- result_payload = self.list_files(page=page, page_size=page_size)
+ category = payload.get("category", "memory")
+ result_payload = self.list_files(page=page, page_size=page_size, category=category)
return {"action": action, "code": 200, "message": "success", "payload": result_payload}
elif action == "content":
filename = payload.get("filename")
if not filename:
return {"action": action, "code": 400, "message": "filename is required", "payload": None}
- result_payload = self.get_content(filename)
+ category = payload.get("category", "memory")
+ result_payload = self.get_content(filename, category=category)
return {"action": action, "code": 200, "message": "success", "payload": result_payload}
else:
@@ -145,18 +159,20 @@ class MemoryService:
# ------------------------------------------------------------------
# internal helpers
# ------------------------------------------------------------------
- def _resolve_path(self, filename: str) -> str:
+ def _resolve_path(self, filename: str, category: str = "memory") -> str:
"""
Safely resolve a filename to its absolute path within the allowed directory.
- ``MEMORY.md`` → ``{workspace_root}/MEMORY.md``
- - ``2026-02-20.md`` → ``{workspace_root}/memory/2026-02-20.md``
+ - ``2026-02-20.md`` (memory) → ``{workspace_root}/memory/2026-02-20.md``
+ - ``2026-02-20.md`` (dream) → ``{workspace_root}/memory/dreams/2026-02-20.md``
- Raises ValueError if the resolved path escapes the allowed directory
- (path traversal protection).
+ Raises ValueError if the resolved path escapes the allowed directory.
"""
if filename == "MEMORY.md":
base_dir = self.workspace_root
+ elif category == "dream":
+ base_dir = os.path.join(self.memory_dir, "dreams")
else:
base_dir = self.memory_dir
diff --git a/channel/web/chat.html b/channel/web/chat.html
index c4aa17f9..4f6d10ac 100644
--- a/channel/web/chat.html
+++ b/channel/web/chat.html
@@ -660,6 +660,16 @@
记忆管理
查看 Agent 记忆文件和内容
+
+
+
+
diff --git a/channel/web/static/css/console.css b/channel/web/static/css/console.css
index de176e3e..9e6c2913 100644
--- a/channel/web/static/css/console.css
+++ b/channel/web/static/css/console.css
@@ -890,15 +890,15 @@
============================================================ */
/* Tab toggle */
-.knowledge-tab {
+.knowledge-tab, .memory-tab {
color: #64748b;
}
-.knowledge-tab.active {
+.knowledge-tab.active, .memory-tab.active {
background: #fff;
color: #334155;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
-.dark .knowledge-tab.active {
+.dark .knowledge-tab.active, .dark .memory-tab.active {
background: rgba(255,255,255,0.1);
color: #e2e8f0;
}
diff --git a/channel/web/static/js/console.js b/channel/web/static/js/console.js
index b6058bd4..c6acfe98 100644
--- a/channel/web/static/js/console.js
+++ b/channel/web/static/js/console.js
@@ -55,6 +55,7 @@ const I18N = {
skills_section_title: '技能', skill_enable: '启用', skill_disable: '禁用',
skill_toggle_error: '操作失败,请稍后再试',
memory_title: '记忆管理', memory_desc: '查看 Agent 记忆文件和内容',
+ memory_tab_files: '记忆文件', memory_tab_dreams: '梦境日记',
memory_loading: '加载记忆文件中...', memory_loading_desc: '记忆文件将显示在此处',
memory_back: '返回列表',
memory_col_name: '文件名', memory_col_type: '类型', memory_col_size: '大小', memory_col_updated: '更新时间',
@@ -139,6 +140,7 @@ const I18N = {
skills_section_title: 'Skills', skill_enable: 'Enable', skill_disable: 'Disable',
skill_toggle_error: 'Operation failed, please try again',
memory_title: 'Memory', memory_desc: 'View agent memory files and contents',
+ memory_tab_files: 'Memory Files', memory_tab_dreams: 'Dream Diary',
memory_loading: 'Loading memory files...', memory_loading_desc: 'Memory files will be displayed here',
memory_back: 'Back to list',
memory_col_name: 'Filename', memory_col_type: 'Type', memory_col_size: 'Size', memory_col_updated: 'Updated',
@@ -2495,12 +2497,20 @@ function toggleSkill(name, currentlyEnabled) {
// Memory View
// =====================================================================
let memoryPage = 1;
+let memoryCategory = 'memory'; // 'memory' | 'dream'
const memoryPageSize = 10;
+function switchMemoryTab(tab) {
+ document.querySelectorAll('.memory-tab').forEach(el => el.classList.remove('active'));
+ document.getElementById('memory-tab-' + tab).classList.add('active');
+ memoryCategory = tab === 'dreams' ? 'dream' : 'memory';
+ loadMemoryView(1);
+}
+
function loadMemoryView(page) {
page = page || 1;
memoryPage = page;
- fetch(`/api/memory?page=${page}&page_size=${memoryPageSize}`).then(r => r.json()).then(data => {
+ fetch(`/api/memory?page=${page}&page_size=${memoryPageSize}&category=${memoryCategory}`).then(r => r.json()).then(data => {
if (data.status !== 'success') return;
const emptyEl = document.getElementById('memory-empty');
const listEl = document.getElementById('memory-list');
@@ -2508,7 +2518,15 @@ function loadMemoryView(page) {
const total = data.total || 0;
if (total === 0) {
- emptyEl.querySelector('p').textContent = currentLang === 'zh' ? '暂无记忆文件' : 'No memory files';
+ const emptyIcon = emptyEl.querySelector('i');
+ const emptyTitle = emptyEl.querySelector('p');
+ if (memoryCategory === 'dream') {
+ emptyIcon.className = 'fas fa-moon text-purple-400 text-xl';
+ emptyTitle.textContent = currentLang === 'zh' ? '暂无梦境日记' : 'No dream diaries yet';
+ } else {
+ emptyIcon.className = 'fas fa-brain text-purple-400 text-xl';
+ emptyTitle.textContent = currentLang === 'zh' ? '暂无记忆文件' : 'No memory files';
+ }
emptyEl.classList.remove('hidden');
listEl.classList.add('hidden');
return;
@@ -2521,10 +2539,15 @@ function loadMemoryView(page) {
files.forEach(f => {
const tr = document.createElement('tr');
tr.className = 'border-b border-slate-100 dark:border-white/5 hover:bg-slate-50 dark:hover:bg-white/5 cursor-pointer transition-colors';
- tr.onclick = () => openMemoryFile(f.filename);
- const typeLabel = f.type === 'global'
- ? 'Global'
- : 'Daily';
+ tr.onclick = () => openMemoryFile(f.filename, memoryCategory);
+ let typeLabel;
+ if (f.type === 'global') {
+ typeLabel = 'Global';
+ } else if (f.type === 'dream') {
+ typeLabel = 'Dream';
+ } else {
+ typeLabel = 'Daily';
+ }
const sizeStr = f.size < 1024 ? f.size + ' B' : (f.size / 1024).toFixed(1) + ' KB';
tr.innerHTML = `
${escapeHtml(f.filename)} |
@@ -2546,8 +2569,9 @@ function loadMemoryView(page) {
}).catch(() => {});
}
-function openMemoryFile(filename) {
- fetch(`/api/memory/content?filename=${encodeURIComponent(filename)}`).then(r => r.json()).then(data => {
+function openMemoryFile(filename, category) {
+ category = category || 'memory';
+ fetch(`/api/memory/content?filename=${encodeURIComponent(filename)}&category=${category}`).then(r => r.json()).then(data => {
if (data.status !== 'success') return;
document.getElementById('memory-panel-list').classList.add('hidden');
const panel = document.getElementById('memory-panel-viewer');
@@ -3444,10 +3468,9 @@ navigateTo = function(viewId) {
if (viewId === 'config') loadConfigView();
else if (viewId === 'skills') loadSkillsView();
else if (viewId === 'memory') {
- // Always start from the list panel when navigating to memory
document.getElementById('memory-panel-viewer').classList.add('hidden');
document.getElementById('memory-panel-list').classList.remove('hidden');
- loadMemoryView(1);
+ switchMemoryTab('files');
}
else if (viewId === 'knowledge') loadKnowledgeView();
else if (viewId === 'channels') loadChannelsView();
diff --git a/channel/web/web_channel.py b/channel/web/web_channel.py
index 5b74c702..49f9081b 100644
--- a/channel/web/web_channel.py
+++ b/channel/web/web_channel.py
@@ -1538,10 +1538,13 @@ class MemoryHandler:
web.header('Content-Type', 'application/json; charset=utf-8')
try:
from agent.memory.service import MemoryService
- params = web.input(page='1', page_size='20')
+ params = web.input(page='1', page_size='20', category='memory')
workspace_root = _get_workspace_root()
service = MemoryService(workspace_root)
- result = service.list_files(page=int(params.page), page_size=int(params.page_size))
+ result = service.list_files(
+ page=int(params.page), page_size=int(params.page_size),
+ category=params.category,
+ )
return json.dumps({"status": "success", **result}, ensure_ascii=False)
except Exception as e:
logger.error(f"[WebChannel] Memory API error: {e}")
@@ -1554,12 +1557,12 @@ class MemoryContentHandler:
web.header('Content-Type', 'application/json; charset=utf-8')
try:
from agent.memory.service import MemoryService
- params = web.input(filename='')
+ params = web.input(filename='', category='memory')
if not params.filename:
return json.dumps({"status": "error", "message": "filename required"})
workspace_root = _get_workspace_root()
service = MemoryService(workspace_root)
- result = service.get_content(params.filename)
+ result = service.get_content(params.filename, category=params.category)
return json.dumps({"status": "success", **result}, ensure_ascii=False)
except ValueError:
return json.dumps({"status": "error", "message": "invalid filename"})