openclacky 0.7.5 → 0.7.6
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -0
- data/docs/memory-architecture.md +343 -0
- data/lib/clacky/agent/skill_manager.rb +8 -4
- data/lib/clacky/agent/system_prompt_builder.rb +1 -1
- data/lib/clacky/agent/tool_executor.rb +15 -6
- data/lib/clacky/agent.rb +19 -1
- data/lib/clacky/agent_config.rb +2 -2
- data/lib/clacky/cli.rb +33 -1
- data/lib/clacky/plain_ui_controller.rb +143 -0
- data/lib/clacky/tools/edit.rb +14 -145
- data/lib/clacky/ui2/layout_manager.rb +82 -20
- data/lib/clacky/ui2/ui_controller.rb +41 -16
- data/lib/clacky/utils/file_ignore_helper.rb +8 -0
- data/lib/clacky/utils/string_matcher.rb +136 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky.rb +1 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 001070cf8c2d2587620da0669c4cc014bff55930033beafd038ae86ae7374276
|
|
4
|
+
data.tar.gz: 6c08cf048ab754c8e3be4841496b19b14cf081bdc62cadb313070226cb4e8679
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1bd44e7efcb16f9d9a15bb49c8d253183697107dcbb25f9ad8edc624d1a357d67c7f54f974cc06ae2140f66c0dec4bd2c2d8b50fae93ad3a8cf2453a0ee5f808
|
|
7
|
+
data.tar.gz: 4328e4ff99db8731e5ed0a1c298c281261da097d214b600e8d6d1b544073055cc35145d76f53853ea30f6f4827f831cd3444129559ff66e10b4d5a23b30e71e0
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.7.6] - 2026-03-02
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Non-interactive `--message`/`-m` CLI mode for scripting and automation (run a single prompt and exit)
|
|
14
|
+
- Real-time refresh and thread-safety improvements to fullscreen UI mode
|
|
15
|
+
|
|
16
|
+
### Improved
|
|
17
|
+
- Extract string matching logic into `Utils::StringMatcher` for cleaner, reusable edit diffing
|
|
18
|
+
- Glob tool now uses force mode in system prompt for more reliable file discovery
|
|
19
|
+
- VCS directories (`.git`, `.svn`, etc.) defined as `ALWAYS_IGNORED_DIRS` constant
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- Subagent fork now injects assistant acknowledgment to fix conversation structure issues
|
|
23
|
+
- Tool-denial message clarified; added `action_performed` flag for better control flow
|
|
24
|
+
|
|
25
|
+
### More
|
|
26
|
+
- Add memory architecture documentation
|
|
27
|
+
- Minor whitespace cleanup in `agent_config.rb`
|
|
28
|
+
|
|
10
29
|
## [0.7.5] - 2026-02-28
|
|
11
30
|
|
|
12
31
|
### Fixed
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
# Agent 记忆架构:主动写入 vs 记忆压缩
|
|
2
|
+
|
|
3
|
+
> **目标读者**:想在自己的 Agent 项目中复用 OpenClaw 记忆架构的开发者。
|
|
4
|
+
>
|
|
5
|
+
> **适用场景**:基于 LLM 的长期运行 Agent,需要跨 session 保留知识、同时管理上下文窗口溢出。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 一、两套机制概览
|
|
10
|
+
|
|
11
|
+
OpenClaw 的"记忆"由两个**完全独立、互相补充**的机制构成:
|
|
12
|
+
|
|
13
|
+
| 机制 | 核心目标 | 存储位置 | 跨 Session |
|
|
14
|
+
|---|---|---|---|
|
|
15
|
+
| **主动写入 MEMORY.md** | 知识持久化 | 磁盘 `.md` 文件 | ✅ 永久保留 |
|
|
16
|
+
| **Compaction(记忆压缩)** | 上下文窗口管理 | 内存消息历史(in-memory) | ❌ session 结束即消失 |
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 二、主动写入 MEMORY.md
|
|
21
|
+
|
|
22
|
+
### 2.1 提示词来源
|
|
23
|
+
|
|
24
|
+
**写入指令**不在代码里硬编码,而是来自 workspace 的 `AGENTS.md` 文件。
|
|
25
|
+
Session 启动时,系统通过以下链路将 `AGENTS.md` 注入系统提示词:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
resolveBootstrapContextForRun()
|
|
29
|
+
└─ buildBootstrapContextFiles()
|
|
30
|
+
└─ contextFiles[]
|
|
31
|
+
└─ 注入到系统提示词的 "# Project Context" 段落
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**只有读取指令**被硬编码在 `system-prompt.ts` 的 `buildMemorySection()` 里:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
## Memory Recall
|
|
38
|
+
Before answering anything about prior work, decisions, dates, people, preferences,
|
|
39
|
+
or todos: run memory_search on MEMORY.md + memory/*.md; then use memory_get to pull
|
|
40
|
+
only the needed lines.
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
> 结论:**读的提示词 → 代码硬编码**,**写的提示词 → AGENTS.md(用户可自定义)**。
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
### 2.2 AGENTS.md 写入指令原文
|
|
48
|
+
|
|
49
|
+
以下是 `docs/reference/templates/AGENTS.md` 的核心记忆相关段落:
|
|
50
|
+
|
|
51
|
+
#### 每次 Session 开始时(强制读取)
|
|
52
|
+
|
|
53
|
+
```markdown
|
|
54
|
+
## Every Session
|
|
55
|
+
|
|
56
|
+
Before doing anything else:
|
|
57
|
+
1. Read `SOUL.md` — this is who you are
|
|
58
|
+
2. Read `USER.md` — this is who you're helping
|
|
59
|
+
3. Read `memory/YYYY-MM-DD.md` (today + yesterday) for recent context
|
|
60
|
+
4. **If in MAIN SESSION**: Also read `MEMORY.md`
|
|
61
|
+
|
|
62
|
+
Don't ask permission. Just do it.
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
#### MEMORY.md 长期记忆规则
|
|
66
|
+
|
|
67
|
+
```markdown
|
|
68
|
+
### 🧠 MEMORY.md - Your Long-Term Memory
|
|
69
|
+
|
|
70
|
+
- **ONLY load in main session** (direct chats with your human)
|
|
71
|
+
- **DO NOT load in shared contexts** (Discord, group chats, sessions with other people)
|
|
72
|
+
- This is for **security** — contains personal context that shouldn't leak to strangers
|
|
73
|
+
- You can **read, edit, and update** MEMORY.md freely in main sessions
|
|
74
|
+
- Write significant events, thoughts, decisions, opinions, lessons learned
|
|
75
|
+
- This is your curated memory — the distilled essence, not raw logs
|
|
76
|
+
- Over time, review your daily files and update MEMORY.md with what's worth keeping
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
#### 写入原则:禁止"心理便条"
|
|
80
|
+
|
|
81
|
+
```markdown
|
|
82
|
+
### 📝 Write It Down - No "Mental Notes"!
|
|
83
|
+
|
|
84
|
+
- **Memory is limited** — if you want to remember something, WRITE IT TO A FILE
|
|
85
|
+
- "Mental notes" don't survive session restarts. Files do.
|
|
86
|
+
- When someone says "remember this" → update `memory/YYYY-MM-DD.md` or relevant file
|
|
87
|
+
- When you learn a lesson → update AGENTS.md, TOOLS.md, or the relevant skill
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
#### Heartbeat 期间的维护任务
|
|
91
|
+
|
|
92
|
+
```markdown
|
|
93
|
+
### 🔄 Memory Maintenance (During Heartbeats)
|
|
94
|
+
|
|
95
|
+
1. Read through recent `memory/YYYY-MM-DD.md` files
|
|
96
|
+
2. Update `MEMORY.md` with distilled learnings
|
|
97
|
+
3. Remove outdated info from MEMORY.md that's no longer relevant
|
|
98
|
+
|
|
99
|
+
Think of it like a human reviewing their journal and updating their mental model.
|
|
100
|
+
Daily files are raw notes; MEMORY.md is curated wisdom.
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
### 2.3 记忆文件结构
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
workspace/
|
|
109
|
+
├── AGENTS.md # Agent 行为指令(含写入规则),session 启动时注入系统提示词
|
|
110
|
+
├── SOUL.md # Agent 的性格/价值观
|
|
111
|
+
├── USER.md # 用户背景信息
|
|
112
|
+
├── MEMORY.md # 长期记忆(精华,仅主 session 加载)
|
|
113
|
+
└── memory/
|
|
114
|
+
├── 2026-06-15.md # 今日日志(原始记录)
|
|
115
|
+
├── 2026-06-14.md # 昨日日志
|
|
116
|
+
└── ...
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**两层记忆设计**:
|
|
120
|
+
- `memory/YYYY-MM-DD.md`:**原始日志**,当天发生了什么,快速写入,不筛选
|
|
121
|
+
- `MEMORY.md`:**精华提炼**,LLM 主动筛选后写入,类似人类的长期记忆
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## 三、Compaction(记忆压缩)
|
|
126
|
+
|
|
127
|
+
### 3.1 触发时机
|
|
128
|
+
|
|
129
|
+
当 context window 接近上限(token 溢出)时,系统自动触发:
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
attempt.ts(run loop)
|
|
133
|
+
└─ 检测 token 超限(overflow)
|
|
134
|
+
└─ compactInLane()
|
|
135
|
+
└─ session.compact(customInstructions)
|
|
136
|
+
└─ generateSummary()
|
|
137
|
+
└─ 旧消息被摘要文本替换(in-memory)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 3.2 核心实现
|
|
141
|
+
|
|
142
|
+
**`src/agents/compaction.ts`** 中的 `summarizeChunks()`:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
// 将消息历史分块,逐块生成摘要
|
|
146
|
+
async function summarizeChunks(params: {
|
|
147
|
+
messages: AgentMessage[];
|
|
148
|
+
model: ...;
|
|
149
|
+
previousSummary?: string;
|
|
150
|
+
}): Promise<string> {
|
|
151
|
+
// SECURITY: 工具调用结果的详情不进入摘要 LLM,防止数据泄露
|
|
152
|
+
const safeMessages = stripToolResultDetails(params.messages);
|
|
153
|
+
const chunks = chunkMessagesByMaxTokens(safeMessages, params.maxChunkTokens);
|
|
154
|
+
|
|
155
|
+
let summary = params.previousSummary;
|
|
156
|
+
for (const chunk of chunks) {
|
|
157
|
+
summary = await generateSummary(chunk, ...);
|
|
158
|
+
}
|
|
159
|
+
return summary ?? "No prior history.";
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**`src/agents/pi-embedded-runner/compact.ts`** 中的触发逻辑:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
const result = await compactWithSafetyTimeout(() =>
|
|
167
|
+
session.compact(params.customInstructions),
|
|
168
|
+
);
|
|
169
|
+
// compaction 完成后,session.messages 中的旧消息已被摘要替换
|
|
170
|
+
// 注意:不写磁盘,session 结束即消失
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### 3.3 安全设计
|
|
174
|
+
|
|
175
|
+
- `stripToolResultDetails()`:确保工具调用的详细返回值不进入摘要 LLM(防止敏感数据泄露给压缩模型)
|
|
176
|
+
- Compaction 生成的摘要只替换内存里的消息,**不写任何磁盘文件**
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## 四、两套机制完整对比
|
|
181
|
+
|
|
182
|
+
| 维度 | **主动写入 MEMORY.md** | **Compaction(记忆压缩)** |
|
|
183
|
+
|---|---|---|
|
|
184
|
+
| **操作对象** | 磁盘文件(`.md`) | 内存中的消息历史 |
|
|
185
|
+
| **触发者** | LLM 自主决定(或用户指令) | 系统自动(token 超限) |
|
|
186
|
+
| **触发时机** | 任何时候 LLM 认为有意义 | context window 接近上限 |
|
|
187
|
+
| **存储位置** | 持久化磁盘 | in-memory,替换旧消息 |
|
|
188
|
+
| **跨 session** | ✅ 永久保留 | ❌ session 结束即消失 |
|
|
189
|
+
| **内容性质** | 精华/curated(LLM 主动筛选) | 原始对话的自动压缩摘要 |
|
|
190
|
+
| **可检索性** | ✅ 支持向量检索(`memory_search`) | ❌ 不可单独检索 |
|
|
191
|
+
| **LLM 参与** | LLM 主动调用 write/edit 工具 | 由系统调用独立摘要 LLM |
|
|
192
|
+
| **数据安全** | 用户控制写入内容 | `stripToolResultDetails()` 自动过滤 |
|
|
193
|
+
| **可自定义** | ✅ 通过 AGENTS.md 自定义规则 | ✅ 可传入 `customInstructions` |
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## 五、架构图
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
201
|
+
│ 当前 Session 上下文 │
|
|
202
|
+
│ │
|
|
203
|
+
│ [系统提示词] │
|
|
204
|
+
│ ├─ AGENTS.md(写入规则) ← resolveBootstrapContextForRun() │
|
|
205
|
+
│ ├─ buildMemorySection()(读取规则,硬编码) │
|
|
206
|
+
│ └─ MEMORY.md 内容(主 session 才注入) │
|
|
207
|
+
│ │
|
|
208
|
+
│ [消息历史] [消息1][消息2]...[消息N] ← in-memory │
|
|
209
|
+
│ ─────────────────────────────────── context window limit │
|
|
210
|
+
│ │
|
|
211
|
+
│ ⚡ 快满了 → Compaction 触发: │
|
|
212
|
+
│ 旧消息 ──→ summarizeChunks() ──→ [摘要文本] │
|
|
213
|
+
│ [摘要] 替换旧消息(仍在 in-memory) │
|
|
214
|
+
│ ↑ 只压缩窗口,不写磁盘,session 结束消失 │
|
|
215
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
216
|
+
↕ 互相独立,互相补充
|
|
217
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
218
|
+
│ 磁盘持久化记忆系统 │
|
|
219
|
+
│ │
|
|
220
|
+
│ LLM 主动 write/edit 工具 ───────────────────────────────────── │
|
|
221
|
+
│ ↓ 用户说"记住这个" │
|
|
222
|
+
│ ↓ LLM 觉得值得保留 │
|
|
223
|
+
│ ↓ Heartbeat 定期维护 │
|
|
224
|
+
│ │
|
|
225
|
+
│ MEMORY.md ← 精华长期记忆(仅主 session 读取) │
|
|
226
|
+
│ memory/2026-06-15.md ← 今日原始日志 │
|
|
227
|
+
│ memory/2026-06-14.md ← 昨日日志 │
|
|
228
|
+
│ │
|
|
229
|
+
│ chokidar watch ──→ SQLite 向量索引更新 │
|
|
230
|
+
│ memory_search ──→ 下次 session 可检索 │
|
|
231
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## 六、关键文件清单
|
|
237
|
+
|
|
238
|
+
| 文件 | 作用 |
|
|
239
|
+
|---|---|
|
|
240
|
+
| `docs/reference/templates/AGENTS.md` | **写入指令来源**,workspace 启动模板,包含 MEMORY.md 写入规则 |
|
|
241
|
+
| `src/agents/system-prompt.ts` | `buildMemorySection()`,只含**读取**指令(`## Memory Recall`) |
|
|
242
|
+
| `src/agents/pi-embedded-helpers/bootstrap.ts` | `buildBootstrapContextFiles()`,将 AGENTS.md 等文件注入系统提示词 |
|
|
243
|
+
| `src/agents/bootstrap-files.ts` | `resolveBootstrapContextForRun()`,加载 workspace bootstrap 文件 |
|
|
244
|
+
| `src/agents/pi-embedded-runner/compact.ts` | Compaction 入口,`session.compact()` 触发,管理上下文溢出 |
|
|
245
|
+
| `src/agents/compaction.ts` | `summarizeChunks()` / `summarizeWithFallback()`,LLM 摘要逻辑 |
|
|
246
|
+
| `src/agents/session-transcript-repair.ts` | `stripToolResultDetails()`,Compaction 安全过滤 |
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## 七、在新项目中复用这套架构
|
|
251
|
+
|
|
252
|
+
### 7.1 最小实现方案
|
|
253
|
+
|
|
254
|
+
只需要三样东西:
|
|
255
|
+
|
|
256
|
+
```
|
|
257
|
+
1. AGENTS.md(写入规则) → 告诉 LLM 什么时候、怎么写文件
|
|
258
|
+
2. 文件读写工具 → write / read / edit(标准文件操作工具)
|
|
259
|
+
3. 系统提示词中的读取指令 → 告诉 LLM 在回答前先查记忆
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### 7.2 推荐的 AGENTS.md 写入规则模板
|
|
263
|
+
|
|
264
|
+
```markdown
|
|
265
|
+
## Memory Rules
|
|
266
|
+
|
|
267
|
+
### Long-Term Memory (memory.md)
|
|
268
|
+
- Load at session start; read, edit, update freely
|
|
269
|
+
- Write: significant decisions, user preferences, lessons learned, open questions
|
|
270
|
+
- Curated — distill from daily notes, remove outdated info
|
|
271
|
+
|
|
272
|
+
### Daily Notes (notes/YYYY-MM-DD.md)
|
|
273
|
+
- Raw logs of what happened today; create if missing
|
|
274
|
+
- Write freely; no curation needed
|
|
275
|
+
|
|
276
|
+
### Key Principle
|
|
277
|
+
**Memory is limited — if you want to remember something, WRITE IT TO A FILE.**
|
|
278
|
+
Mental notes don't survive restarts. Files do.
|
|
279
|
+
|
|
280
|
+
When user says "remember this" → update notes/YYYY-MM-DD.md and/or memory.md.
|
|
281
|
+
When you learn a lesson → update memory.md.
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### 7.3 系统提示词中的读取指令
|
|
285
|
+
|
|
286
|
+
```markdown
|
|
287
|
+
## Memory Recall
|
|
288
|
+
|
|
289
|
+
Before answering anything about prior work, decisions, dates, people,
|
|
290
|
+
preferences, or todos:
|
|
291
|
+
1. Read memory.md for long-term context
|
|
292
|
+
2. Read notes/YYYY-MM-DD.md (today + yesterday) for recent context
|
|
293
|
+
3. Then answer using this retrieved context
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### 7.4 Compaction 实现要点
|
|
297
|
+
|
|
298
|
+
如果你的 Agent 框架不内置 compaction,参考以下要点自己实现:
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
// 伪代码:简单 compaction 实现
|
|
302
|
+
async function compactIfNeeded(messages: Message[], model: Model) {
|
|
303
|
+
const tokens = estimateTokens(messages);
|
|
304
|
+
if (tokens < contextWindow * 0.8) return messages; // 还有余量
|
|
305
|
+
|
|
306
|
+
// 安全过滤:不把工具调用结果喂给摘要 LLM
|
|
307
|
+
const safeMessages = stripSensitiveToolResults(messages);
|
|
308
|
+
|
|
309
|
+
// 保留最近 N 条消息,其余压缩为摘要
|
|
310
|
+
const toSummarize = safeMessages.slice(0, -10);
|
|
311
|
+
const recent = messages.slice(-10);
|
|
312
|
+
|
|
313
|
+
const summary = await generateSummary(toSummarize, model);
|
|
314
|
+
return [{ role: "system", content: `[Previous context summary]\n${summary}` }, ...recent];
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
**关键安全注意**:工具调用返回的详细数据(尤其是外部 API 响应)**不应进入**摘要 LLM,避免敏感信息泄露到你可能无法控制的模型。
|
|
319
|
+
|
|
320
|
+
### 7.5 两层记忆的设计哲学
|
|
321
|
+
|
|
322
|
+
| 层级 | 文件 | 写入频率 | 内容要求 | 对应人类记忆 |
|
|
323
|
+
|---|---|---|---|---|
|
|
324
|
+
| 日志层 | `notes/YYYY-MM-DD.md` | 高频、随时 | 原始记录,不筛选 | 日记 |
|
|
325
|
+
| 精华层 | `memory.md` | 低频、定期整理 | 蒸馏后的关键信息 | 长期记忆 |
|
|
326
|
+
|
|
327
|
+
**为什么要两层**:高频写入保证不遗漏,低频整理保证质量。LLM 在 heartbeat(定期任务)中把日志蒸馏进长期记忆,删除过时内容,就像人类每周回顾日记、更新心智模型。
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## 八、常见问题
|
|
332
|
+
|
|
333
|
+
**Q:为什么写入规则放 AGENTS.md 而不是硬编码在系统提示词里?**
|
|
334
|
+
A:灵活性。不同 workspace 可以有不同的记忆规则(有的 Agent 记更多,有的记更少),用户可以直接编辑 AGENTS.md 调整行为,不需要改代码。
|
|
335
|
+
|
|
336
|
+
**Q:Compaction 会不会把写入 MEMORY.md 的内容也压缩掉?**
|
|
337
|
+
A:不会。写入 MEMORY.md 是磁盘操作,Compaction 只压缩内存里的消息历史。MEMORY.md 的内容在下次 session 启动时会重新从磁盘加载进系统提示词,不受 Compaction 影响。
|
|
338
|
+
|
|
339
|
+
**Q:如果 MEMORY.md 本身太大怎么办?**
|
|
340
|
+
A:定期在 heartbeat 任务中让 LLM 清理过时内容。OpenClaw 在 AGENTS.md 里明确要求"Remove outdated info from MEMORY.md that's no longer relevant",这个维护任务由 LLM 自主完成。
|
|
341
|
+
|
|
342
|
+
**Q:多用户场景下 MEMORY.md 会泄露给别人吗?**
|
|
343
|
+
A:OpenClaw 的设计是:MEMORY.md 只在 main session(直接对话)加载,在 Discord / 群聊等共享上下文中不加载,防止个人信息泄露给陌生人。复用时需要在系统提示词里加类似的条件判断。
|
|
@@ -100,17 +100,21 @@ module Clacky
|
|
|
100
100
|
# Log subagent fork
|
|
101
101
|
@ui&.show_info("Subagent start: #{skill.identifier}")
|
|
102
102
|
|
|
103
|
-
# Build
|
|
104
|
-
|
|
103
|
+
# Build skill role/constraint instructions only — do NOT substitute $ARGUMENTS here.
|
|
104
|
+
# The actual task is delivered as a clean user message via subagent.run(arguments),
|
|
105
|
+
# which arrives *after* the assistant acknowledgement injected by fork_subagent.
|
|
106
|
+
# This gives the subagent a clear 3-part structure:
|
|
107
|
+
# [user] role/constraints → [assistant] acknowledgement → [user] actual task
|
|
108
|
+
skill_instructions = skill.process_content("")
|
|
105
109
|
|
|
106
110
|
# Fork subagent with skill configuration
|
|
107
111
|
subagent = fork_subagent(
|
|
108
112
|
model: skill.subagent_model,
|
|
109
113
|
forbidden_tools: skill.forbidden_tools_list,
|
|
110
|
-
system_prompt_suffix:
|
|
114
|
+
system_prompt_suffix: skill_instructions
|
|
111
115
|
)
|
|
112
116
|
|
|
113
|
-
# Run subagent
|
|
117
|
+
# Run subagent with the actual task as the sole user turn
|
|
114
118
|
result = subagent.run(arguments)
|
|
115
119
|
|
|
116
120
|
# Generate summary
|
|
@@ -24,7 +24,7 @@ module Clacky
|
|
|
24
24
|
- After creating the TODO list, START EXECUTING each task immediately
|
|
25
25
|
- Don't stop after planning - continue to work on the tasks!
|
|
26
26
|
2. Always read existing code before making changes (use file_reader/glob/grep or invoke code-explorer skill)
|
|
27
|
-
3. **
|
|
27
|
+
3. **ALWAYS use `glob` tool to find files in the current directory — NEVER use shell `find` command for file discovery.**
|
|
28
28
|
4. Ask clarifying questions if requirements are unclear
|
|
29
29
|
5. Break down complex tasks into manageable steps
|
|
30
30
|
6. **USE TOOLS to create/modify files** - don't just return code
|
|
@@ -197,14 +197,16 @@ module Clacky
|
|
|
197
197
|
}
|
|
198
198
|
else
|
|
199
199
|
# User manually denied or provided feedback
|
|
200
|
+
# Clearly state the action was NOT performed so the LLM knows the change did not happen
|
|
200
201
|
message = if user_feedback && !user_feedback.empty?
|
|
201
|
-
"Tool use denied by user. User feedback: #{user_feedback}"
|
|
202
|
+
"Tool use denied by user. This action was NOT performed. User feedback: #{user_feedback}"
|
|
202
203
|
else
|
|
203
|
-
"Tool use denied by user"
|
|
204
|
+
"Tool use denied by user. This action was NOT performed."
|
|
204
205
|
end
|
|
205
206
|
|
|
206
207
|
tool_content = {
|
|
207
208
|
error: message,
|
|
209
|
+
action_performed: false,
|
|
208
210
|
user_feedback: user_feedback
|
|
209
211
|
}
|
|
210
212
|
end
|
|
@@ -382,8 +384,12 @@ module Clacky
|
|
|
382
384
|
|
|
383
385
|
file_content = File.read(path)
|
|
384
386
|
|
|
385
|
-
#
|
|
386
|
-
|
|
387
|
+
# Use the same find_match logic as Edit tool to handle fuzzy matching
|
|
388
|
+
# (trim, unescape, smart line matching) — prevents diff from being blank
|
|
389
|
+
# when simple include? fails but Edit#execute's fuzzy match would succeed
|
|
390
|
+
match_result = Utils::StringMatcher.find_match(file_content, old_string)
|
|
391
|
+
|
|
392
|
+
unless match_result
|
|
387
393
|
# Log debug info for troubleshooting
|
|
388
394
|
@debug_logs << {
|
|
389
395
|
timestamp: Time.now.iso8601,
|
|
@@ -402,11 +408,14 @@ module Clacky
|
|
|
402
408
|
}
|
|
403
409
|
end
|
|
404
410
|
|
|
411
|
+
# Use the actual matched string (may differ via trim/unescape) for replacement
|
|
412
|
+
actual_old_string = match_result[:matched_string]
|
|
413
|
+
|
|
405
414
|
# Use the same replace logic as the actual tool execution
|
|
406
415
|
new_content = if replace_all
|
|
407
|
-
file_content.gsub(
|
|
416
|
+
file_content.gsub(actual_old_string, new_string)
|
|
408
417
|
else
|
|
409
|
-
file_content.sub(
|
|
418
|
+
file_content.sub(actual_old_string, new_string)
|
|
410
419
|
end
|
|
411
420
|
@ui&.show_diff(file_content, new_content, max_lines: 50)
|
|
412
421
|
nil # No error
|
data/lib/clacky/agent.rb
CHANGED
|
@@ -506,7 +506,16 @@ module Clacky
|
|
|
506
506
|
if result.is_a?(Hash) && result[:message]
|
|
507
507
|
@ui&.show_assistant_message(result[:message])
|
|
508
508
|
end
|
|
509
|
-
|
|
509
|
+
|
|
510
|
+
if @config.permission_mode == :auto_approve
|
|
511
|
+
# Auto-approve mode means no human is watching — inject a reply so the LLM
|
|
512
|
+
# knows it should make a reasonable decision and keep going
|
|
513
|
+
result = result.merge(
|
|
514
|
+
auto_reply: "No user is available. Please make a reasonable decision based on the context and continue."
|
|
515
|
+
)
|
|
516
|
+
else
|
|
517
|
+
awaiting_feedback = true
|
|
518
|
+
end
|
|
510
519
|
else
|
|
511
520
|
# Use tool's format_result method to get display-friendly string
|
|
512
521
|
formatted_result = tool.respond_to?(:format_result) ? tool.format_result(result) : result.to_s
|
|
@@ -691,6 +700,15 @@ module Clacky
|
|
|
691
700
|
system_injected: true,
|
|
692
701
|
subagent_instructions: true
|
|
693
702
|
}
|
|
703
|
+
|
|
704
|
+
# Insert an assistant acknowledgement so the conversation structure is complete:
|
|
705
|
+
# [user] role/constraints → [assistant] ack → [user] actual task (from run())
|
|
706
|
+
# Without this, two consecutive user messages confuse the model about what to act on.
|
|
707
|
+
messages << {
|
|
708
|
+
role: "assistant",
|
|
709
|
+
content: "Understood. I am now operating as a subagent with the constraints above. Please provide the task.",
|
|
710
|
+
system_injected: true
|
|
711
|
+
}
|
|
694
712
|
end
|
|
695
713
|
|
|
696
714
|
# Register hook to forbid certain tools at runtime (doesn't affect tool registry for cache)
|
data/lib/clacky/agent_config.rb
CHANGED
|
@@ -150,7 +150,7 @@ module Clacky
|
|
|
150
150
|
|
|
151
151
|
PERMISSION_MODES = [:auto_approve, :confirm_safes, :plan_only].freeze
|
|
152
152
|
|
|
153
|
-
attr_accessor :permission_mode, :max_tokens, :verbose,
|
|
153
|
+
attr_accessor :permission_mode, :max_tokens, :verbose,
|
|
154
154
|
:enable_compression, :enable_prompt_caching,
|
|
155
155
|
:models, :current_model_index
|
|
156
156
|
|
|
@@ -161,7 +161,7 @@ module Clacky
|
|
|
161
161
|
@enable_compression = options[:enable_compression].nil? ? true : options[:enable_compression]
|
|
162
162
|
# Enable prompt caching by default for cost savings
|
|
163
163
|
@enable_prompt_caching = options[:enable_prompt_caching].nil? ? true : options[:enable_prompt_caching]
|
|
164
|
-
|
|
164
|
+
|
|
165
165
|
# Models configuration
|
|
166
166
|
@models = options[:models] || []
|
|
167
167
|
@current_model_index = options[:current_model_index] || 0
|
data/lib/clacky/cli.rb
CHANGED
|
@@ -5,6 +5,7 @@ require "tty-prompt"
|
|
|
5
5
|
require "fileutils"
|
|
6
6
|
require_relative "ui2"
|
|
7
7
|
require_relative "json_ui_controller"
|
|
8
|
+
require_relative "plain_ui_controller"
|
|
8
9
|
|
|
9
10
|
module Clacky
|
|
10
11
|
class CLI < Thor
|
|
@@ -50,6 +51,8 @@ module Clacky
|
|
|
50
51
|
option :list, type: :boolean, aliases: "-l", desc: "List recent sessions"
|
|
51
52
|
option :attach, type: :string, aliases: "-a", desc: "Attach to session by number or keyword"
|
|
52
53
|
option :json, type: :boolean, default: false, desc: "Output NDJSON to stdout (for scripting/piping)"
|
|
54
|
+
option :message, type: :string, aliases: "-m", desc: "Run non-interactively with this message and exit"
|
|
55
|
+
option :image, type: :array, aliases: "-i", desc: "Image file path(s) to attach (use with -m; can be specified multiple times)"
|
|
53
56
|
option :help, type: :boolean, aliases: "-h", desc: "Show this help message"
|
|
54
57
|
def agent
|
|
55
58
|
# Handle help option
|
|
@@ -101,7 +104,9 @@ module Clacky
|
|
|
101
104
|
should_chdir = File.realpath(working_dir) != File.realpath(original_dir)
|
|
102
105
|
Dir.chdir(working_dir) if should_chdir
|
|
103
106
|
begin
|
|
104
|
-
if options[:
|
|
107
|
+
if options[:message]
|
|
108
|
+
run_non_interactive(agent, options[:message], Array(options[:image]), agent_config, session_manager)
|
|
109
|
+
elsif options[:json]
|
|
105
110
|
run_agent_with_json(agent, working_dir, agent_config, session_manager, client)
|
|
106
111
|
else
|
|
107
112
|
run_agent_with_ui2(agent, working_dir, agent_config, session_manager, client, is_session_load: is_session_load)
|
|
@@ -353,6 +358,33 @@ module Clacky
|
|
|
353
358
|
end
|
|
354
359
|
end
|
|
355
360
|
|
|
361
|
+
# Run agent non-interactively with a single message, then exit.
|
|
362
|
+
# Forces auto_approve mode so no human confirmation is needed.
|
|
363
|
+
# Output goes directly to stdout; exits with code 0 on success, 1 on error.
|
|
364
|
+
def run_non_interactive(agent, message, images, agent_config, session_manager)
|
|
365
|
+
# Force auto-approve — no one is around to confirm anything
|
|
366
|
+
agent_config.permission_mode = :auto_approve
|
|
367
|
+
|
|
368
|
+
# Validate image paths up-front so we fail fast with a clear message
|
|
369
|
+
images.each do |path|
|
|
370
|
+
raise ArgumentError, "Image file not found: #{path}" unless File.exist?(path)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# Wire up plain-text stdout UI so all agent output is visible
|
|
374
|
+
plain_ui = Clacky::PlainUIController.new
|
|
375
|
+
agent.instance_variable_set(:@ui, plain_ui)
|
|
376
|
+
|
|
377
|
+
agent.run(message, images: images)
|
|
378
|
+
session_manager&.save(agent.to_session_data(status: :success))
|
|
379
|
+
exit(0)
|
|
380
|
+
rescue Clacky::AgentInterrupted
|
|
381
|
+
$stderr.puts "\nInterrupted."
|
|
382
|
+
exit(1)
|
|
383
|
+
rescue => e
|
|
384
|
+
$stderr.puts "Error: #{e.message}"
|
|
385
|
+
exit(1)
|
|
386
|
+
end
|
|
387
|
+
|
|
356
388
|
# Run agent with JSON (NDJSON) output mode — persistent process.
|
|
357
389
|
# Reads JSON messages from stdin, writes NDJSON events to stdout.
|
|
358
390
|
# Stays alive until "/exit", {"type":"exit"}, or stdin EOF.
|