memocode 0.2.3__tar.gz → 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. {memocode-0.2.3 → memocode-0.3.2}/PKG-INFO +36 -5
  2. {memocode-0.2.3 → memocode-0.3.2}/README.md +35 -4
  3. {memocode-0.2.3 → memocode-0.3.2}/control/brain.py +260 -41
  4. memocode-0.3.2/control/chatmem/compressor.py +176 -0
  5. {memocode-0.2.3 → memocode-0.3.2}/control/chatmem/context_manager.py +132 -10
  6. memocode-0.3.2/control/chatmem/memory/consolidation.py +114 -0
  7. {memocode-0.2.3 → memocode-0.3.2}/control/chatmem/memory/core_memory.py +70 -26
  8. memocode-0.3.2/control/chatmem/memory/recent_memory.py +165 -0
  9. {memocode-0.2.3 → memocode-0.3.2}/memocode.egg-info/PKG-INFO +36 -5
  10. {memocode-0.2.3 → memocode-0.3.2}/pyproject.toml +1 -1
  11. {memocode-0.2.3 → memocode-0.3.2}/run.py +86 -54
  12. {memocode-0.2.3 → memocode-0.3.2}/safety/safety.py +9 -3
  13. memocode-0.2.3/control/chatmem/compressor.py +0 -288
  14. memocode-0.2.3/control/chatmem/memory/consolidation.py +0 -95
  15. memocode-0.2.3/control/chatmem/memory/recent_memory.py +0 -153
  16. {memocode-0.2.3 → memocode-0.3.2}/control/__init__.py +0 -0
  17. {memocode-0.2.3 → memocode-0.3.2}/control/audit.py +0 -0
  18. {memocode-0.2.3 → memocode-0.3.2}/control/chatmem/__init__.py +0 -0
  19. {memocode-0.2.3 → memocode-0.3.2}/control/chatmem/cli.py +0 -0
  20. {memocode-0.2.3 → memocode-0.3.2}/control/chatmem/config.py +0 -0
  21. {memocode-0.2.3 → memocode-0.3.2}/control/chatmem/mcp_server.py +0 -0
  22. {memocode-0.2.3 → memocode-0.3.2}/control/chatmem/memory/__init__.py +0 -0
  23. {memocode-0.2.3 → memocode-0.3.2}/control/chatmem/memory/forgetting.py +0 -0
  24. {memocode-0.2.3 → memocode-0.3.2}/control/chatmem/server.py +0 -0
  25. {memocode-0.2.3 → memocode-0.3.2}/control/chatmem/token_counter.py +0 -0
  26. {memocode-0.2.3 → memocode-0.3.2}/control/fmt.py +0 -0
  27. {memocode-0.2.3 → memocode-0.3.2}/control/llm.py +0 -0
  28. {memocode-0.2.3 → memocode-0.3.2}/control/planner.py +0 -0
  29. {memocode-0.2.3 → memocode-0.3.2}/control/project_manager.py +0 -0
  30. {memocode-0.2.3 → memocode-0.3.2}/memocode.egg-info/SOURCES.txt +0 -0
  31. {memocode-0.2.3 → memocode-0.3.2}/memocode.egg-info/dependency_links.txt +0 -0
  32. {memocode-0.2.3 → memocode-0.3.2}/memocode.egg-info/entry_points.txt +0 -0
  33. {memocode-0.2.3 → memocode-0.3.2}/memocode.egg-info/requires.txt +0 -0
  34. {memocode-0.2.3 → memocode-0.3.2}/memocode.egg-info/top_level.txt +0 -0
  35. {memocode-0.2.3 → memocode-0.3.2}/safety/__init__.py +0 -0
  36. {memocode-0.2.3 → memocode-0.3.2}/safety/backup.py +0 -0
  37. {memocode-0.2.3 → memocode-0.3.2}/safety/policy.py +0 -0
  38. {memocode-0.2.3 → memocode-0.3.2}/setup.cfg +0 -0
  39. {memocode-0.2.3 → memocode-0.3.2}/tools/__init__.py +0 -0
  40. {memocode-0.2.3 → memocode-0.3.2}/tools/file.py +0 -0
  41. {memocode-0.2.3 → memocode-0.3.2}/tools/loader.py +0 -0
  42. {memocode-0.2.3 → memocode-0.3.2}/tools/registry.py +0 -0
  43. {memocode-0.2.3 → memocode-0.3.2}/tools/shell.py +0 -0
  44. {memocode-0.2.3 → memocode-0.3.2}/tools/web.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memocode
3
- Version: 0.2.3
3
+ Version: 0.3.2
4
4
  Summary: Personal AI coding agent with memory, tool execution, and safety controls
5
5
  Author: AssassinCHN
6
6
  Requires-Python: >=3.11
@@ -18,9 +18,9 @@ Personal AI coding agent CLI with memory, tool execution, and safety controls.
18
18
  ## Features
19
19
 
20
20
  - **Agentic tool-use loop** — Native function calling (OpenAI / Anthropic compatible), stuck detection, auto mode for fully autonomous task execution
21
- - **Memory system** — Persistent conversation memory with consolidation, forgetting curve, and cross-session injection
21
+ - **Multi-layer memory system** — Cross-session persistence with structured compression, on-demand recall, and forgetting curve
22
22
  - **Project management** — Multiple projects with separate memory DBs and working directories
23
- - **Built-in tools** — Shell execution, file read/write/edit, glob, grep, web fetch
23
+ - **Built-in tools** — Shell execution, file read/write/edit, glob, grep, web fetch, memory search
24
24
  - **Safety layer** — Human mode (zone checks + confirmation prompts) / Auto mode (whitelist + work_dir hard boundaries) / Bypass mode (session-only, explicit confirmation required)
25
25
  - **Extensible** — Drop Python files into `~/.mcode/tools/` for custom tools
26
26
 
@@ -46,7 +46,7 @@ Configure your model in `~/.mcode/agent.json` (created on first run):
46
46
  "model": "MiniMax-M2.7",
47
47
  "base_url": "https://api.minimaxi.com/v1",
48
48
  "api_key_env": "MINIMAX_API_KEY",
49
- "context_window": 1000000,
49
+ "context_window": 65536,
50
50
  "extra_body": {"reasoning_split": true}
51
51
  }
52
52
  }
@@ -93,11 +93,34 @@ mcode --verbose # Show full tracebacks on errors
93
93
  | `/undo` | Undo the last run — restore code and/or rewind conversation |
94
94
  | `/rollback` | Restore a specific backed-up file (file only, for audit) |
95
95
  | `/rewind [N]` | Rewind conversation to turn N (conversation only) |
96
- | `/profile` | Show/edit core memory (user profile) |
96
+ | `/memory` | Show core memory (user profile) |
97
+ | `/memory set <key> <value>` | Write a core memory entry |
98
+ | `/memory del <key>` | Delete a core memory entry |
99
+ | `/memory pin <key>` | Pin an entry (stability=999, never forgets) |
97
100
  | | |
98
101
  | `!<command>` | Run shell command directly (safety-checked) |
99
102
  | `@<path>` | Attach file contents to your message |
100
103
 
104
+ ## Memory System
105
+
106
+ Mcode maintains four memory layers that persist across sessions:
107
+
108
+ | Layer | Scope | Contents | Updated |
109
+ |-------|-------|----------|---------|
110
+ | **Core memory** | Global (all projects) | User traits: communication style, autonomy preference | Every compression + session end |
111
+ | **Project memory** | Per-project | Decisions, architecture, progress, conventions | Every compression + session end |
112
+ | **Recent memory** | Per-project | Compressed summaries of past sessions | Session end; grows indefinitely |
113
+ | **Session history** | Per-project | Current session verbatim + older turns compressed | Each turn |
114
+
115
+ **How recall works:**
116
+ - Recent session summaries are automatically injected into every turn (newest-first, 4k token cap)
117
+ - The LLM calls `memory_search` when it needs context from older sessions — it decides when and what to search
118
+ - Project memory and core memory are always present in the system prefix (prompt-cache eligible)
119
+
120
+ **Compression:** When the session context reaches 50% of `context_window`, old turns are replaced with a structured summary (topics, decisions, progress, pending, key context). Raw code and config values are excluded — only decisions and rationale are preserved.
121
+
122
+ **Forgetting:** Core memory entries decay over time (Ebbinghaus curve). Entries not reinforced by the judge gradually fade, preventing stale user traits from persisting indefinitely.
123
+
101
124
  ## Auto Mode
102
125
 
103
126
  Auto mode runs the agent fully autonomously — no confirmation prompts, hard safety boundaries (whitelist-only shell commands, writes restricted to `work_dir`).
@@ -131,6 +154,7 @@ Verify by running: <test command>
131
154
  | `glob` | Find files by pattern (`**/*.py`) |
132
155
  | `grep` | Search file content by regex |
133
156
  | `web_fetch` | Fetch a URL, returns readable text (HTML stripped) |
157
+ | `memory_search` | Search past session summaries for relevant context |
134
158
 
135
159
  ## Custom Tools
136
160
 
@@ -167,6 +191,13 @@ mcode/
167
191
  │ ├── project_manager.py # Project registry
168
192
  │ ├── audit.py # Audit log
169
193
  │ └── chatmem/ # Memory system
194
+ │ ├── context_manager.py # Session history, compression, injection
195
+ │ ├── compressor.py # LLM-based structured summarization
196
+ │ └── memory/
197
+ │ ├── core_memory.py # User traits (global, with forgetting)
198
+ │ ├── recent_memory.py # Cross-session summaries (per-project)
199
+ │ ├── consolidation.py # Periodic pattern extraction → core memory
200
+ │ └── forgetting.py # Ebbinghaus decay for core memory
170
201
  ├── tools/
171
202
  │ ├── file.py # file_read/write/edit, glob, grep
172
203
  │ ├── shell.py # shell_exec
@@ -6,9 +6,9 @@ Personal AI coding agent CLI with memory, tool execution, and safety controls.
6
6
  ## Features
7
7
 
8
8
  - **Agentic tool-use loop** — Native function calling (OpenAI / Anthropic compatible), stuck detection, auto mode for fully autonomous task execution
9
- - **Memory system** — Persistent conversation memory with consolidation, forgetting curve, and cross-session injection
9
+ - **Multi-layer memory system** — Cross-session persistence with structured compression, on-demand recall, and forgetting curve
10
10
  - **Project management** — Multiple projects with separate memory DBs and working directories
11
- - **Built-in tools** — Shell execution, file read/write/edit, glob, grep, web fetch
11
+ - **Built-in tools** — Shell execution, file read/write/edit, glob, grep, web fetch, memory search
12
12
  - **Safety layer** — Human mode (zone checks + confirmation prompts) / Auto mode (whitelist + work_dir hard boundaries) / Bypass mode (session-only, explicit confirmation required)
13
13
  - **Extensible** — Drop Python files into `~/.mcode/tools/` for custom tools
14
14
 
@@ -34,7 +34,7 @@ Configure your model in `~/.mcode/agent.json` (created on first run):
34
34
  "model": "MiniMax-M2.7",
35
35
  "base_url": "https://api.minimaxi.com/v1",
36
36
  "api_key_env": "MINIMAX_API_KEY",
37
- "context_window": 1000000,
37
+ "context_window": 65536,
38
38
  "extra_body": {"reasoning_split": true}
39
39
  }
40
40
  }
@@ -81,11 +81,34 @@ mcode --verbose # Show full tracebacks on errors
81
81
  | `/undo` | Undo the last run — restore code and/or rewind conversation |
82
82
  | `/rollback` | Restore a specific backed-up file (file only, for audit) |
83
83
  | `/rewind [N]` | Rewind conversation to turn N (conversation only) |
84
- | `/profile` | Show/edit core memory (user profile) |
84
+ | `/memory` | Show core memory (user profile) |
85
+ | `/memory set <key> <value>` | Write a core memory entry |
86
+ | `/memory del <key>` | Delete a core memory entry |
87
+ | `/memory pin <key>` | Pin an entry (stability=999, never forgets) |
85
88
  | | |
86
89
  | `!<command>` | Run shell command directly (safety-checked) |
87
90
  | `@<path>` | Attach file contents to your message |
88
91
 
92
+ ## Memory System
93
+
94
+ Mcode maintains four memory layers that persist across sessions:
95
+
96
+ | Layer | Scope | Contents | Updated |
97
+ |-------|-------|----------|---------|
98
+ | **Core memory** | Global (all projects) | User traits: communication style, autonomy preference | Every compression + session end |
99
+ | **Project memory** | Per-project | Decisions, architecture, progress, conventions | Every compression + session end |
100
+ | **Recent memory** | Per-project | Compressed summaries of past sessions | Session end; grows indefinitely |
101
+ | **Session history** | Per-project | Current session verbatim + older turns compressed | Each turn |
102
+
103
+ **How recall works:**
104
+ - Recent session summaries are automatically injected into every turn (newest-first, 4k token cap)
105
+ - The LLM calls `memory_search` when it needs context from older sessions — it decides when and what to search
106
+ - Project memory and core memory are always present in the system prefix (prompt-cache eligible)
107
+
108
+ **Compression:** When the session context reaches 50% of `context_window`, old turns are replaced with a structured summary (topics, decisions, progress, pending, key context). Raw code and config values are excluded — only decisions and rationale are preserved.
109
+
110
+ **Forgetting:** Core memory entries decay over time (Ebbinghaus curve). Entries not reinforced by the judge gradually fade, preventing stale user traits from persisting indefinitely.
111
+
89
112
  ## Auto Mode
90
113
 
91
114
  Auto mode runs the agent fully autonomously — no confirmation prompts, hard safety boundaries (whitelist-only shell commands, writes restricted to `work_dir`).
@@ -119,6 +142,7 @@ Verify by running: <test command>
119
142
  | `glob` | Find files by pattern (`**/*.py`) |
120
143
  | `grep` | Search file content by regex |
121
144
  | `web_fetch` | Fetch a URL, returns readable text (HTML stripped) |
145
+ | `memory_search` | Search past session summaries for relevant context |
122
146
 
123
147
  ## Custom Tools
124
148
 
@@ -155,6 +179,13 @@ mcode/
155
179
  │ ├── project_manager.py # Project registry
156
180
  │ ├── audit.py # Audit log
157
181
  │ └── chatmem/ # Memory system
182
+ │ ├── context_manager.py # Session history, compression, injection
183
+ │ ├── compressor.py # LLM-based structured summarization
184
+ │ └── memory/
185
+ │ ├── core_memory.py # User traits (global, with forgetting)
186
+ │ ├── recent_memory.py # Cross-session summaries (per-project)
187
+ │ ├── consolidation.py # Periodic pattern extraction → core memory
188
+ │ └── forgetting.py # Ebbinghaus decay for core memory
158
189
  ├── tools/
159
190
  │ ├── file.py # file_read/write/edit, glob, grep
160
191
  │ ├── shell.py # shell_exec
@@ -68,6 +68,8 @@ def _tool_hint(name: str, args: dict) -> str:
68
68
  return dim(args.get("pattern", ""))
69
69
  if name == "web_fetch":
70
70
  return dim(args.get("url", ""))
71
+ if name == "memory_search":
72
+ return dim(args.get("query", ""))
71
73
  return ""
72
74
 
73
75
 
@@ -93,42 +95,112 @@ def _print_tool_header_simple(name: str, args: dict):
93
95
  print(dim(f" {extra}"))
94
96
 
95
97
 
96
- def _resolve_task_paths(tasks: list, work_dir: str) -> list:
97
- """Legacy helper kept for any remaining callers."""
98
- result = []
99
- for t in tasks:
100
- path_args = {
101
- "file_read": ["path"], "file_write": ["path"],
102
- }.get(t.tool, [])
103
- if not path_args:
104
- result.append(t)
105
- continue
106
- args = dict(t.args)
107
- for arg_name in path_args:
108
- p = args.get(arg_name, "")
109
- if (isinstance(p, str) and p
110
- and not os.path.isabs(p)
111
- and not p.startswith("~")
112
- and "task_" not in p): # skip {{task_N_output}} placeholders
113
- args[arg_name] = os.path.join(work_dir, p)
114
- result.append(Task(
115
- id=t.id, tool=t.tool, args=args,
116
- description=t.description, depends_on=t.depends_on,
117
- reversible=t.reversible, backup_paths=t.backup_paths,
118
- ))
119
- return result
98
+
99
+
100
+ _PROJECT_MEMORY_JSON = os.path.join(_DATA_DIR, "project_memory.json")
101
+
102
+ _DEFAULT_PROJECT_MEMORY = {
103
+ "dimensions": {
104
+ "architecture": {"label": "Tech stack, modules, structure", "stability": 40.0},
105
+ "decisions": {"label": "Confirmed design decisions and rationale", "stability": 35.0},
106
+ "conventions": {"label": "Naming, interface, format conventions", "stability": 35.0},
107
+ "context": {"label": "Project background, goals, constraints", "stability": 30.0},
108
+ "progress": {"label": "Current phase, pending key tasks", "stability": 20.0},
109
+ "completed": {"label": "Completed tasks and milestones", "stability": 25.0},
110
+ },
111
+ }
112
+
113
+ def _build_project_judge_prompt(dimensions: dict) -> str:
114
+ dim_lines = "\n".join(f"- {name}: {cfg.label}" for name, cfg in dimensions.items())
115
+ dim_names = ", ".join(f'"{name}"' for name in dimensions)
116
+ return f"""You are a project memory editor. Review the conversation and update the project's knowledge base.
117
+
118
+ Project memory dimensions:
119
+ {dim_lines}
120
+
121
+ Existing project memory:
122
+ {{existing}}
123
+
124
+ New conversation:
125
+ {{text}}
126
+
127
+ Rules:
128
+ - One entry per dimension. Key must equal the dimension name (e.g. key="decisions").
129
+ - For each dimension, choose one action:
130
+ SKIP — no new project-relevant information for this dimension.
131
+ SUPPLEMENT — new info extends existing (merge into one updated value).
132
+ UPDATE — existing info is clearly superseded or corrected.
133
+ DELETE — the entire dimension entry is explicitly invalidated with nothing to replace it
134
+ (e.g. "we're dropping the X feature entirely", "forget everything about the old auth system").
135
+ Use DELETE only when UPDATE with a replacement value is not possible.
136
+ - Special rules by dimension:
137
+ completed: always SUPPLEMENT (append new completions, never erase old ones). Never DELETE.
138
+ progress: UPDATE freely when the current phase or pending tasks change.
139
+ ONLY record what the user explicitly stated. Do NOT infer, extrapolate,
140
+ or add typical/expected follow-up tasks not mentioned in the conversation.
141
+ decisions: SUPPLEMENT by default; UPDATE only if the user makes an explicit statement reversing a prior decision
142
+ (e.g. "we're switching from X to Y", "decided to drop X"). Do NOT update based on the assistant
143
+ discussing alternatives, or the user asking hypothetical questions about other options.
144
+ architecture/conventions: SUPPLEMENT by default; UPDATE on explicit refactor or migration.
145
+ - Return ONLY entries that changed. Omit SKIPped ones.
146
+ - Exclude: user personal preferences, one-time questions, reasoning processes.
147
+ - Never fabricate or infer information not present in the conversation.
148
+
149
+ Return a JSON array (empty if nothing changed):
150
+ [{{"key": "<dimension_name>", "value": "<merged or new value>", "dimension": "<dimension_name>", "action": "supplement|update|delete"}}]
151
+ Dimension name must be one of: {dim_names}
152
+
153
+ Output JSON only:"""
154
+
155
+
156
+ def _build_project_judge_prompt_zh(dimensions: dict) -> str:
157
+ dim_lines = "\n".join(f"- {name}: {cfg.label}" for name, cfg in dimensions.items())
158
+ dim_names = ", ".join(f'"{name}"' for name in dimensions)
159
+ return f"""你是一个项目记忆编辑器。审查对话内容,更新项目知识库。
160
+
161
+ 项目记忆维度:
162
+ {dim_lines}
163
+
164
+ 当前项目记忆:
165
+ {{existing}}
166
+
167
+ 新对话:
168
+ {{text}}
169
+
170
+ 规则:
171
+ - 每个维度只有一条记录,key 等于维度名(如 key="decisions")。
172
+ - 对每个维度,选择以下操作之一:
173
+ 跳过 — 该维度没有新的项目相关信息。
174
+ 补充 — 新信息扩展了已有内容(合并为一个更新后的 value)。
175
+ 修正 — 已有信息被明确推翻或替换。
176
+ 删除 — 整个维度条目被明确作废且无替代内容
177
+ (如"我们整个X功能都不做了"、"忘掉旧认证系统的一切")。
178
+ 仅在无法用修正+新值替代时才使用删除。
179
+ - 各维度特殊规则:
180
+ completed:始终补充(追加新完成内容,不删除旧记录)。不可删除。
181
+ progress:当前阶段或待办任务变化时,直接修正。
182
+ 只记录用户明确说出的内容,严禁推断、外推或添加"典型后续任务"。
183
+ 用户未提及的待办事项一律不写。
184
+ decisions:默认补充;仅当用户明确陈述推翻先前决策时才修正
185
+ (如"我们从X换成Y了"、"决定放弃X")。不得因为
186
+ AI 讨论替代方案、或用户询问假设性问题而修正。
187
+ architecture/conventions:默认补充;仅在明确重构或迁移时修正。
188
+ - 只返回发生变化的条目,跳过的不返回。
189
+ - 不包括:用户个人偏好、一次性问题、推理过程。
190
+ - 严禁捏造或推断对话中未出现的信息。
191
+
192
+ 返回JSON数组(无变化则返回空数组):
193
+ [{{"key": "<维度名>", "value": "<合并后或新的内容>", "dimension": "<维度名>", "action": "supplement|update|delete"}}]
194
+ 维度名必须是以下之一:{dim_names}
195
+
196
+ 只输出JSON:"""
120
197
 
121
198
 
122
199
  _DEFAULT_CHATMEM = {
123
200
  "llm": {},
124
201
  "dimensions": {
125
- "identity": {"label": "Who I am", "stability": 50.0},
126
- "values": {"label": "What I believe", "stability": 40.0},
127
- "goals": {"label": "What I want", "stability": 30.0},
128
- "preferences": {"label": "Likes / dislikes", "stability": 25.0},
129
- "capabilities": {"label": "What I can do", "stability": 30.0},
130
- "emotional": {"label": "How I react", "stability": 20.0},
131
- "autobiography": {"label": "Key experiences", "stability": 35.0},
202
+ "communication": {"label": "Expression style, language preference, tone, detail level", "stability": 45.0},
203
+ "autonomy": {"label": "How much decision authority delegated to LLM: executes directly vs presents options vs validates", "stability": 40.0},
132
204
  },
133
205
  }
134
206
 
@@ -170,11 +242,11 @@ _DEFAULT_AGENT = {
170
242
  }
171
243
 
172
244
 
173
- def _init_data_dir() -> tuple[str, str]:
245
+ def _init_data_dir() -> tuple[str, str, str]:
174
246
  """
175
- Ensure ~/.mcode/ exists with agent.json and chatmem.json.
247
+ Ensure ~/.mcode/ exists with agent.json, chatmem.json, project_memory.json.
176
248
  Migrates from control/ if old files exist and new ones don't.
177
- Returns (agent_json_path, chatmem_json_path).
249
+ Returns (agent_json_path, chatmem_json_path, project_memory_json_path).
178
250
  """
179
251
  import json as _json
180
252
  import shutil as _shutil
@@ -184,6 +256,7 @@ def _init_data_dir() -> tuple[str, str]:
184
256
 
185
257
  agent_path = os.path.join(_DATA_DIR, "agent.json")
186
258
  chatmem_path = os.path.join(_DATA_DIR, "chatmem.json")
259
+ project_memory_path = _PROJECT_MEMORY_JSON
187
260
 
188
261
  # Migrate agent.json from old location
189
262
  old_agent = os.path.join(_CONTROL, "agent.json")
@@ -205,7 +278,13 @@ def _init_data_dir() -> tuple[str, str]:
205
278
  _json.dump(_DEFAULT_CHATMEM, f, indent=2, ensure_ascii=False)
206
279
  f.write("\n")
207
280
 
208
- return agent_path, chatmem_path
281
+ # Create project_memory.json with default project dimensions
282
+ if not os.path.exists(project_memory_path):
283
+ with open(project_memory_path, "w") as f:
284
+ _json.dump(_DEFAULT_PROJECT_MEMORY, f, indent=2, ensure_ascii=False)
285
+ f.write("\n")
286
+
287
+ return agent_path, chatmem_path, project_memory_path
209
288
 
210
289
 
211
290
  def _sync_active_model(agent_cfg: dict, chatmem_json_path: str | None):
@@ -227,7 +306,7 @@ def _sync_active_model(agent_cfg: dict, chatmem_json_path: str | None):
227
306
  profile = model_cfg
228
307
  agent_cfg["active_model"] = active
229
308
  # Save the updated active_model back to agent.json
230
- agent_path, _ = _init_data_dir()
309
+ agent_path, *_ = _init_data_dir()
231
310
  with open(agent_path) as f:
232
311
  import json as _json
233
312
  data = _json.load(f)
@@ -260,7 +339,7 @@ class Brain:
260
339
  no_lock: bool = False,
261
340
  ):
262
341
  # Ensure ~/.mcode/ exists; migrate old files if needed
263
- agent_path, chatmem_path = _init_data_dir()
342
+ agent_path, chatmem_path, project_memory_path = _init_data_dir()
264
343
 
265
344
  self._agent_cfg_path = agent_path
266
345
  self._config_path = config_path or chatmem_path
@@ -320,6 +399,11 @@ class Brain:
320
399
  else:
321
400
  self.projects.find_or_create_for_cwd()
322
401
 
402
+ # Project memory — per-project CoreMemory in the same project DB
403
+ self.project_memory = self._make_project_memory(
404
+ cfg, _api_key, project_memory_path
405
+ )
406
+
323
407
  # Memory layer — one DB per project, core DB shared
324
408
  self.memory = ContextManager.create(
325
409
  config=cfg,
@@ -327,6 +411,13 @@ class Brain:
327
411
  db_path=self.projects.project_db_path,
328
412
  core_db_path=self.projects.core_db_path,
329
413
  no_lock=no_lock,
414
+ extra_context_fn=(
415
+ self.project_memory.to_context_string if self.project_memory else None
416
+ ),
417
+ on_compress_fn=(
418
+ self._run_project_judge if self.project_memory else None
419
+ ),
420
+ auto_recall=False,
330
421
  )
331
422
 
332
423
  # Audit log
@@ -341,6 +432,7 @@ class Brain:
341
432
  self.registry.register(GLOB_TOOL)
342
433
  self.registry.register(GREP_TOOL)
343
434
  self.registry.register(WEB_FETCH_TOOL)
435
+ self.registry.register(self._make_memory_search_tool())
344
436
  load_external_tools(self.registry)
345
437
 
346
438
  self._max_tool_iter: int = agent_cfg.get("max_tool_iter", 40)
@@ -406,7 +498,8 @@ class Brain:
406
498
  "If the user asks you to fetch a URL or HTTP endpoint, call web_fetch — never fabricate the response. "
407
499
  "Before editing a file, always read it first with file_read, then proceed to make the edit without stopping. "
408
500
  "For targeted changes use file_edit; for new files use file_write. "
409
- "If a task requires multiple tool calls, keep calling tools until the task is fully done — do not report completion until all actions have been executed via tools."
501
+ "If a task requires multiple tool calls, keep calling tools until the task is fully done — do not report completion until all actions have been executed via tools. "
502
+ "If the user asks about something that may have been discussed in a previous session and you don't have that information in the current context, call memory_search before concluding it is unknown or undecided."
410
503
  )
411
504
  user_msg = [
412
505
  {"role": "system", "content": _system},
@@ -627,7 +720,8 @@ class Brain:
627
720
  return True
628
721
 
629
722
  if self.auto_mode:
630
- whitelist = self._agent_cfg.get("auto_whitelist", None)
723
+ user_wl = self._agent_cfg.get("auto_whitelist", None)
724
+ whitelist = list(set(_safety.DEFAULT_AUTO_WHITELIST) | set(user_wl)) if user_wl else None
631
725
  result = _safety.check_auto(task, work_dir=work_dir, whitelist=whitelist)
632
726
  if result.violation:
633
727
  print(red(f"\n🛑 AUTO-BLOCKED {result.reason}"))
@@ -789,20 +883,115 @@ class Brain:
789
883
  # ------------------------------------------------------------------
790
884
 
791
885
  def switch_project(self, name: str):
792
- self.memory.end_session()
886
+ self.end_session()
793
887
  self.projects.switch_project(name)
794
888
  self._reinit_memory()
795
889
 
890
+ def _make_memory_search_tool(self):
891
+ """Build a memory_search Tool that searches recent_memory for relevant past context."""
892
+ from tools.registry import Tool, ToolSchema
893
+ recent_memory = self.memory.recent_memory
894
+
895
+ def _search(query: str, top_k: int = 5) -> str:
896
+ results = recent_memory.search(query, top_k=top_k, min_score=0.0)
897
+ if not results:
898
+ return "No relevant memory found."
899
+ return "\n\n---\n\n".join(results)
900
+
901
+ return Tool(
902
+ schema=ToolSchema(
903
+ name="memory_search",
904
+ description=(
905
+ "Search compressed summaries of past conversation sessions for relevant context. "
906
+ "ALWAYS call this before saying something is unknown, undecided, or not recorded — "
907
+ "the information may exist in a previous session even if it is not in the current context. "
908
+ "Use key terms from the user's question as the query.\n"
909
+ "搜索过去对话的压缩摘要,获取相关上下文。"
910
+ "在说某事未知、未决定或未记录之前,必须先调用此工具——"
911
+ "即使当前上下文中没有该信息,它也可能存在于之前的会话中。"
912
+ "用用户问题中的关键词作为查询词。"
913
+ ),
914
+ parameters={
915
+ "type": "object",
916
+ "properties": {
917
+ "query": {
918
+ "type": "string",
919
+ "description": "Key terms to search for (e.g. '8080 端口', 'authentication design')",
920
+ },
921
+ "top_k": {
922
+ "type": "integer",
923
+ "description": "Max results to return (default 5)",
924
+ "default": 5,
925
+ },
926
+ },
927
+ "required": ["query"],
928
+ },
929
+ ),
930
+ fn=_search,
931
+ )
932
+
933
+ def _make_project_memory(self, cfg: "ContextConfig", api_key: str, project_memory_path: str):
934
+ """Instantiate a CoreMemory for project-scoped knowledge."""
935
+ import json as _json
936
+ from chatmem.memory.core_memory import CoreMemory
937
+ from chatmem.config import DimensionConfig
938
+
939
+ try:
940
+ with open(project_memory_path) as f:
941
+ pm_data = _json.load(f)
942
+ dims = {
943
+ k: DimensionConfig(label=v["label"], stability=float(v["stability"]))
944
+ for k, v in pm_data.get("dimensions", {}).items()
945
+ }
946
+ except Exception:
947
+ dims = {}
948
+ if not dims:
949
+ return None
950
+
951
+ compress_model = cfg.llm.compress_model or cfg.llm.model
952
+ _THINKING_KEYS = {"reasoning_split", "thinking", "reasoning_effort"}
953
+ extra_body = (
954
+ {k: v for k, v in cfg.llm.extra_body.items() if k not in _THINKING_KEYS}
955
+ if cfg.llm.extra_body else None
956
+ ) or None
957
+ provider = getattr(cfg.llm, "provider", "openai") or "openai"
958
+
959
+ pm = CoreMemory(
960
+ db_path=self.projects.project_db_path,
961
+ api_key=api_key,
962
+ model=compress_model,
963
+ base_url=cfg.llm.base_url,
964
+ dimensions=dims,
965
+ extra_body=extra_body,
966
+ provider=provider,
967
+ context_label="Project Memory",
968
+ )
969
+
970
+ # Override judge prompts with project-specific framing
971
+ pm._judge_prompt_en = _build_project_judge_prompt(dims)
972
+ pm._judge_prompt_zh = _build_project_judge_prompt_zh(dims)
973
+ return pm
974
+
796
975
  def _reinit_memory(self):
797
976
  if getattr(self, "memory", None) is not None:
798
977
  self.memory.close()
799
978
  cfg = ContextConfig.from_json(self._config_path) if self._config_path else ContextConfig()
979
+ api_key = cfg.llm.resolve_api_key()
980
+ self.project_memory = self._make_project_memory(cfg, api_key, _PROJECT_MEMORY_JSON)
800
981
  self.memory = ContextManager.create(
801
982
  config=cfg,
802
- api_key=cfg.llm.resolve_api_key(),
983
+ api_key=api_key,
803
984
  db_path=self.projects.project_db_path,
804
985
  core_db_path=self.projects.core_db_path,
986
+ extra_context_fn=(
987
+ self.project_memory.to_context_string if self.project_memory else None
988
+ ),
989
+ on_compress_fn=(
990
+ self._run_project_judge if self.project_memory else None
991
+ ),
992
+ auto_recall=False,
805
993
  )
994
+ self.registry.register(self._make_memory_search_tool())
806
995
 
807
996
  def _save_agent_cfg(self):
808
997
  """Persist current _agent_cfg to agent.json."""
@@ -815,5 +1004,35 @@ class Brain:
815
1004
  # Session lifecycle
816
1005
  # ------------------------------------------------------------------
817
1006
 
1007
+ def _run_project_judge(self, text: str):
1008
+ if not self.project_memory:
1009
+ return
1010
+ import logging as _logging
1011
+ logger = _logging.getLogger("chatmem")
1012
+ try:
1013
+ written = self.project_memory.judge_and_update(text)
1014
+ if written:
1015
+ for e in written:
1016
+ if e.get("action") == "delete":
1017
+ logger.info("[project] judge deleted key=%r", e["key"])
1018
+ else:
1019
+ logger.info("[project] judge wrote key=%r dim=%s value=%r",
1020
+ e["key"], e.get("dimension"), e["value"][:60])
1021
+ else:
1022
+ logger.info("[project] judge returned empty")
1023
+ except Exception as exc:
1024
+ logger.warning("[project] judge error: %s", exc)
1025
+
818
1026
  def end_session(self):
1027
+ # Capture verbatim history before end_session clears it
1028
+ verbatim = [m for m in self.memory._history if not m.get("_is_summary")]
1029
+ conv_text = "\n".join(
1030
+ f"{m['role']}: {m.get('content', '')}" for m in verbatim
1031
+ ) if verbatim else ""
1032
+
819
1033
  self.memory.end_session()
1034
+
1035
+ if conv_text:
1036
+ threading.Thread(
1037
+ target=self._run_project_judge, args=(conv_text,), daemon=True
1038
+ ).start()