Follow up PR #265: refine chapters, diagrams, and add S20 (#283)

* feat: s01-s14 docs quality overhaul — tool pipeline, single-agent, knowledge & resilience

Rewrite code.py and README (zh/en/ja) for s01-s14, each chapter building
incrementally on the previous. Key fixes across chapters:

- s01-s04: agent loop, tool dispatch, permission pipeline, hooks
- s05-s08: todo write, subagent, skill loading, context compact
- s09-s11: memory system, system prompt assembly, error recovery
- s12-s14: task graph, background tasks, cron scheduler

All chapters CC source-verified. Code inherits fixes forward (PROMPT_SECTIONS,
json.dumps cache, real-state context, can_start dep protection, etc.).

* feat: s15-s19 docs quality overhaul — multi-agent platform: teams, protocols, autonomy, worktree, MCP tools

Rewrite code.py and README (zh/en/ja) for s15-s19, the multi-agent platform
chapters. Each chapter inherits all previous fixes and adds one mechanism:

- s15: agent teams (TeamCreate, teammate threads, shared task list)
- s16: team protocols (plan approval, shutdown handshake, consume_inbox)
- s17: autonomous agents (idle polling, auto-claim, consume_lead_inbox)
- s18: worktree isolation (git worktree, bind_task, cwd switching, safety)
- s19: MCP tools (MCPClient, normalize_mcp_name, assemble_tool_pool, no cache)

All appendix source code references verified against CC source. Config priority
corrected: claude.ai < plugin < user < project < local.

* fix: 5 regressions across s05-s19 — glob safety, todo validation, memory extraction, protocol types, dep crash

- s05-s09: glob results now filter with is_relative_to(WORKDIR) (inherited from s02)
- s06-s08: todo_write validates content/status required fields (inherited from s05)
- s09: extract_memories uses pre-compression snapshot instead of compacted messages
- s16: submit_plan docstring clarifies protocol-only (not code-level gate)
- s17-s19: match_response restores type mismatch validation (from s16)
- s17-s19: claim_task deps list handles missing dep files without crashing

* fix: s12 Todo V2 logic reversal, s14/s15 cron range validation, s18/s19 worktree name validation

- s12 README (zh/en/ja): fix Todo V2 direction — interactive defaults to Task,
  non-interactive/SDK defaults to TodoWrite. Fix env var name to
  CLAUDE_CODE_ENABLE_TASKS (not TODO_V2).
- s14/s15: add _validate_cron_field with per-field range checks (minute 0-59,
  hour 0-23, dom 1-31, month 1-12, dow 0-6), step > 0, range lo <= hi.
  Replace old try/except validation that only caught exceptions.
- s18/s19: add validate_worktree_name() to remove_worktree and keep_worktree,
  not just create_worktree.

* fix: align s16-s19 teaching tool consistency

* fix pr265 chapter diagrams

* Add comprehensive s20 harness chapter

* Fix chapter smoke test regressions

* Clarify README tutorial track transition

---------

Co-authored-by: Haoran <bill-billion@outlook.com>
This commit is contained in:
gui-yue
2026-05-20 21:45:38 +08:00
committed by GitHub
parent c354cf7721
commit 1baf1aca5a
174 changed files with 35833 additions and 353 deletions

279
s09_memory/README.en.md Normal file
View File

@@ -0,0 +1,279 @@
# s09: Memory — Compression Loses Details, Keep a Layer That Doesn't
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
s01 → ... → s07 → s08 → `s09` → [s10](../s10_system_prompt/) → s11 → ... → s20
> *"Compression loses details, keep a layer that doesn't"* — File store + index + on-demand loading, across compactions, across sessions.
>
> **Harness Layer**: Memory — knowledge that survives compaction and sessions.
---
## The Problem
s08's autoCompact preserves current goals, remaining work, and user constraints in the summary, but details get lost: "use tabs not spaces" might get simplified to "user has code style preferences". And when you start a new session, even the summary is gone.
LLMs have no persistent state; all information lives in the context window. When context fills up, it gets compressed, and compression is lossy. What's needed is a storage layer that doesn't participate in compression and persists across sessions.
---
## The Solution
![Memory Overview](images/memory-overview.en.svg)
The s08 compression pipeline is preserved, focusing on memory. Storage uses the filesystem: a `.memory/` directory where each memory is a `.md` file with YAML frontmatter (`name` / `description` / `type`). When files accumulate, an index is needed: `MEMORY.md` holds one link per line and gets injected into the SYSTEM.
Key design: the index stays in SYSTEM prompt (cacheable by prompt cache), file content is injected on demand (matched by filename/description to the current conversation, without breaking the cache). Writing has two paths: the user explicitly says "remember", or extraction runs in the background after each turn. When files accumulate, periodic consolidation deduplicates.
Four memory types, each answering a different question:
| Type | Answers | Example |
|------|---------|---------|
| user | Who you are | "Use tabs not spaces" |
| feedback | How to work | "Don't mock the database" |
| project | What's happening | "Auth rewrite is compliance-driven" |
| reference | Where to find things | "Pipeline bugs are in Linear INGEST" |
---
## How It Works
![Memory Subsystems](images/memory-subsystems.en.svg)
### Storage: Markdown Files + Index
Each memory is a `.md` file with YAML frontmatter for metadata:
```markdown
---
name: user-preference-tabs
description: User prefers tabs for indentation
type: user
---
User prefers using tabs, not spaces, for indentation.
**Why:** Consistency with existing codebase conventions.
**How to apply:** Always use tabs when writing or editing files.
```
`MEMORY.md` is the index, one link per line:
```markdown
- [user-preference-tabs](user-preference-tabs.md) — User prefers tabs for indentation
```
Writing a new memory automatically rebuilds the index:
```python
def write_memory_file(name, mem_type, description, body):
slug = name.lower().replace(" ", "-")
filepath = MEMORY_DIR / f"{slug}.md"
filepath.write_text(
f"---\nname: {name}\ndescription: {description}\ntype: {mem_type}\n---\n\n{body}\n"
)
_rebuild_index()
```
### Loading: Two Paths
**Path 1: Index in SYSTEM.** `build_system()` reads `MEMORY.md` every turn and injects the memory catalog into the SYSTEM prompt. The index in SYSTEM can be cached by prompt cache, avoiding resending it every turn.
**Path 2: Relevant memories on demand.** Before each LLM call, `load_memories()` sends the recent conversation and the memory catalog (name + description) to the LLM as a lightweight side-query, selects relevant filenames, then reads and injects their contents. Capped at 5 to control cost.
```python
def select_relevant_memories(messages, max_items=5):
files = list_memory_files()
if not files:
return []
# Build catalog: "0: user-preference-tabs — User prefers tabs..."
catalog = "\n".join(f"{i}: {f['name']}{f['description']}" for i, f in enumerate(files))
response = client.messages.create(model=MODEL, messages=[{"role": "user",
"content": f"Select relevant memory indices. Return JSON array.\n\n"
f"Recent conversation:\n{recent}\n\nMemory catalog:\n{catalog}"}],
max_tokens=200)
indices = json.loads(re.search(r'\[.*?\]', response.content[0].text).group())
return [files[i]["filename"] for i in indices if 0 <= i < len(files)]
```
If the side-query fails (API error, JSON parse failure), it falls back to keyword matching on name + description.
### Writing: Extraction After Each Turn
Users don't always say "remember this". Preferences are usually scattered across normal dialogue: "tabs are better than spaces", "let's use single quotes from now on".
`extract_memories()` runs when each turn ends, triggered when the model stops without a tool_use (indicating the conversation has reached a natural break):
```python
# In agent_loop:
if response.stop_reason != "tool_use":
extract_memories(messages) # Extract new memories from recent dialogue
consolidate_memories() # Check if consolidation is needed
return
```
Before extraction, existing memories are checked to avoid duplicates. The extraction prompt asks the LLM to return a JSON array of `{name, type, description, body}`, writing files only when genuinely new information is found.
```python
def extract_memories(messages):
dialogue = format_recent_messages(messages[-10:])
existing = "\n".join(f"- {m['name']}: {m['description']}" for m in list_memory_files())
prompt = (
"Extract user preferences, constraints, or project facts.\n"
"Return JSON array: [{name, type, description, body}].\n"
"If nothing new or already covered, return [].\n\n"
f"Existing memories:\n{existing}\n\nDialogue:\n{dialogue[:4000]}"
)
# ... parse response, write files ...
```
### Consolidation: Low-Frequency Deduplication
Memory files accumulate. `consolidate_memories()` triggers when the file count reaches a threshold (default 10), asking the LLM to deduplicate, merge contradictions, and prune stale memories:
```python
CONSOLIDATE_THRESHOLD = 10
def consolidate_memories():
files = list_memory_files()
if len(files) < CONSOLIDATE_THRESHOLD:
return # Too few, not worth consolidating
# Send all memories to LLM, get back deduplicated list
# Replace all files with consolidated results
```
CC calls this process **Dream**, with four gates in practice: time interval, scan throttle, session count, file lock. The teaching version simplifies to a file-count threshold.
### What Memory Stores
Memory stores information that remains useful across sessions: user preferences, recurring feedback, project background, common entry points, and investigation clues. It focuses on "what will be useful later" and brings that information back through an index plus on-demand loading.
Session memory focuses on continuity inside one session: what context should survive after compaction. The two work together: Memory handles long-term knowledge; session memory handles the current session across compaction.
---
## Changes From s08
| Component | Before (s08) | After (s09) |
|-----------|-------------|-------------|
| Memory capability | None (preferences degrade with compaction) | Storage + loading + extraction + consolidation |
| New functions | — | write_memory_file, select_relevant_memories, load_memories, extract_memories, consolidate_memories |
| Storage | — | .memory/MEMORY.md index + .memory/*.md files |
| Tools | bash, read, write, edit, glob, todo_write, task, load_skill, compact (9) | bash, read_file, write_file, edit_file, glob, task (6) |
| Loop | Only compression each turn | Memory injection + compression + post-turn extraction + periodic consolidation |
---
## Try It
```sh
cd learn-claude-code
python s09_memory/code.py
```
Try these prompts (enter across multiple turns, observe memory accumulation and loading):
1. `I prefer using tabs for indentation, not spaces. Remember that.`
2. `Create a Python file called test.py` (observe whether the Agent uses tabs)
3. `What did I tell you about my preferences?` (observe whether the Agent remembers)
4. `I also prefer single quotes over double quotes for strings.`
What to watch for: Does `[Memory: extracted N new memories]` appear after each turn? Are `.md` files generated in `.memory/`? Is `MEMORY.md` index updated? Does the Agent automatically load previous memories in new conversations?
---
## What's Next
Memory, compression, and tools are all in place. But the system prompt is still a hardcoded string. Adding a new tool means manually adding a description; switching projects means rewriting the whole prompt. Prompts should be assembled at runtime.
s10 System Prompt → segments + runtime assembly. Different projects, different tools, different prompts.
<details>
<summary>Deep Dive Into CC Source Code</summary>
> The following is based on analysis of CC source code under `src/` in `memdir/`, `services/`, `utils/`, `query/`. Line numbers verified against source.
### Source Code Paths
| File | Lines | Responsibility |
|------|-------|---------------|
| `memdir/memdir.ts` | 507 | Core: MEMORY.md definition (`34-38`), memory behavior instructions distinguishing memory/plan/tasks (`199-266`), `loadMemoryPrompt()` three paths (`419-490`) |
| `memdir/findRelevantMemories.ts` | 141 | Sonnet side-query memory selection (`18-24` system prompt, `97-122` call logic) |
| `memdir/memoryTypes.ts` | 271 | Type definitions, frontmatter fields |
| `memdir/memoryScan.ts` | — | Scan .md files, exclude MEMORY.md, read frontmatter, max 200 files, sorted by mtime desc (`35-94`) |
| `services/extractMemories/extractMemories.ts` | 615 | Forked agent extraction, restricted permissions, `skipTranscript: true`, `maxTurns: 5` (`371-427`) |
| `services/autoDream/autoDream.ts` | 324 | Dream consolidation, four-layer gating (`63-66` defaults, `130-190` gating, `224-233` forked agent) |
| `services/SessionMemory/sessionMemory.ts` | 495 | Session-level memory management |
| `services/compact/sessionMemoryCompact.ts` | — | Session memory lightweight summary, thresholds 10K/5/40K (`56-61`) |
| `utils/attachments.ts` | — | Injection budget: 200 lines / 4096 bytes per file, 60KB per session (`269-288`); find relevant memory by query (`2196-2241`) |
| `query.ts` | — | Memory prefetch at start of each user turn (`301-304`), non-blocking collection (`1592-1614`) |
| `query/stopHooks.ts` | — | Stop hook fire-and-forget triggers extraction and Dream (`141-155`) |
### Memory Selection: LLM, Not Embedding
CC uses **Sonnet itself to select** (`findRelevantMemories.ts`), not embedding vector similarity:
1. `memoryScan.ts` scans all `.md` files in `.memory/` (excluding MEMORY.md), max 200 files, sorted by mtime descending
2. Lists all memory files' `name` + `description` as a catalog
3. Sends to Sonnet side-query: "Select truly useful memories by name and description (max 5). Skip if unsure."
4. Sonnet returns `{ selected_memories: ["file1.md", ...] }`
5. Selected files' full contents are read (≤ 200 lines / 4096 bytes per file) and injected. Total session budget: 60KB
At the start of each user turn, `query.ts:301-304` starts memory prefetch (async); after tool execution, `1592-1614` collects completed results non-blocking.
### Extraction Timing: Stop Hook, Not After autoCompact
Trigger location (`stopHooks.ts:141-155`): inside `handleStopHooks()`, fire-and-forget triggers extraction and Dream. The teaching version places extraction in the `stop_reason != "tool_use"` branch, matching the direction.
CC's extraction runs via forked agent (`extractMemories.ts:371-427`): restricted permissions, `skipTranscript: true`, `maxTurns: 5`. Also has overlap protection: if the main Agent already wrote memory files, extraction is skipped.
### Memory File Format
CC uses Markdown + YAML frontmatter, consistent with the teaching version. Four types: `user`, `feedback`, `project`, `reference`.
`memdir.ts:34-38` defines index constraints: `MEMORY.md` max 200 lines / 25KB. `memdir.ts:199-266` builds memory behavior instructions, explicitly distinguishing memory from plan and tasks. Storage location: `~/.claude/projects/<sanitized-git-root>/memory/`.
### Dream: Four-Layer Gating
Not "triggered when idle" or "consolidate when count is enough", but four gates (`autoDream.ts`, defaults `63-66`, gating logic `130-190`):
1. **Time gate**: ≥ 24 hours since last consolidation
2. **Scan throttle**: Avoid frequent filesystem scans
3. **Session gate**: ≥ 5 session transcripts modified since last consolidation
4. **Lock gate**: No other process currently consolidating (`.consolidate-lock` file)
The merge itself runs via forked agent (`224-233`): locate → collect recent signals → merge and write files → prune and update index. Lock file mtime serves as lastConsolidatedAt. Crash recovery: lock auto-expires after 1 hour.
### User Memory vs Session Memory
| | User Memory | Session Memory |
|---|---|---|
| Persistence | Cross-session | Single session |
| Storage | Multiple .md files in `memory/` | `session-memory/<id>/memory.md` |
| Loaded into | system prompt | compact summary |
| Purpose | Cross-session knowledge accumulation | Cross-compact context continuity |
sessionMemoryCompact (mentioned in s08) uses Session Memory: before autoCompact, it reads the session memory file and, if sufficient (≥ 10K tokens, ≥ 5 text messages, ≤ 40K tokens, `sessionMemoryCompact.ts:56-61`), uses it as a summary without calling the LLM.
### Where the Real Implementation Is More Complex
- **Feature flags**: Memory features have multiple feature gate layers
- **Team memory**: Shared team memories, `loadMemoryPrompt()` has a dedicated path (not covered in teaching version)
- **KAIROS**: Timing-aware memory extraction strategy, daily-log mode in `loadMemoryPrompt()`
- **Prompt cache**: Memory injection must account for prompt cache TTL, avoiding full system prompt rewrites each turn
- **File locks**: Concurrency control for multi-process scenarios
- **Memory prefetch**: Async prefetch, non-blocking main flow
### Teaching Version Simplifications Are Intentional
- LLM side-query → LLM side-query + keyword fallback: teaching version keeps LLM selection, adds fallback path
- Memory JSON → Markdown + frontmatter: teaching version matches CC
- Stop hook trigger → `stop_reason != "tool_use"` branch: same direction
- Four-layer gating → file-count threshold: teaching version lacks transcript system and multi-session concepts
- Forked agent + restricted permissions → direct call: teaching version has no subprocess isolation
</details>
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->

279
s09_memory/README.ja.md Normal file
View File

@@ -0,0 +1,279 @@
# s09: Memory — 圧縮は詳細を失う、失わない層が必要
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
s01 → ... → s07 → s08 → `s09` → [s10](../s10_system_prompt/) → s11 → ... → s20
> *"圧縮は詳細を失う、失わない層が必要"* — ファイルストア + インデックス + オンデマンド読み込み。圧縮を越え、セッションを越えて。
>
> **Harness レイヤー**: 記憶 — 圧縮とセッションを越える知識の蓄積。
---
## 課題
s08 の autoCompact は現在の目標、残りの作業、ユーザーの制約をサマリに保持するが、詳細は失われる:「タブでインデント、スペース不可」が「ユーザーにコードスタイルの好みあり」と簡略化される。そして新しいセッションを開始すると、サマリすらない。
LLM には永続状態がなく、すべての情報はコンテキストウィンドウ内にある。コンテキストが満杯になれば圧縮され、圧縮は非可逆。圧縮に参加せず、セッションを越えて保持されるストレージ層が必要。
---
## ソリューション
![Memory Overview](images/memory-overview.ja.svg)
s08 の圧縮パイプラインを維持し、記憶に焦点を当てる。ストレージにはファイルシステムを採用:`.memory/` ディレクトリに各記憶を `.md` ファイルとして保存、YAML frontmatter`name` / `description` / `type`)付き。ファイルが増えたらインデックスが必要:`MEMORY.md` に 1 行 1 リンクを記録し、SYSTEM に注入。
重要な設計:インデックスは SYSTEM prompt に常駐prompt cache でキャッシュ可能、ファイル内容はオンデマンド注入filename/description で現在の会話にマッチ、cache を破壊しない)。書き込みは 2 つのパス:ユーザーが明示的に「覚えて」と言うか、毎ターン終了後にバックグラウンドで抽出。ファイルが蓄積されたら、定期的に整理して重複排除。
4 種類の記憶、それぞれ異なる質問に答える:
| タイプ | 何に答えるか | 例 |
|--------|-------------|-----|
| user | あなたは誰か | "タブでスペース不可" |
| feedback | どう作業するか | "DB をモックしない" |
| project | 何が起きているか | "auth 書き直しはコンプライアンス主導" |
| reference | どこで探すか | "パイプラインのバグは Linear INGEST" |
---
## 仕組み
![Memory Subsystems](images/memory-subsystems.ja.svg)
### ストレージMarkdown ファイル + インデックス
各記憶は `.md` ファイル、YAML frontmatter でメタデータを記録:
```markdown
---
name: user-preference-tabs
description: User prefers tabs for indentation
type: user
---
User prefers using tabs, not spaces, for indentation.
**Why:** Consistency with existing codebase conventions.
**How to apply:** Always use tabs when writing or editing files.
```
`MEMORY.md` はインデックス、1 行に 1 リンク:
```markdown
- [user-preference-tabs](user-preference-tabs.md) — User prefers tabs for indentation
```
新しい記憶を書き込むとインデックスを自動再構築:
```python
def write_memory_file(name, mem_type, description, body):
slug = name.lower().replace(" ", "-")
filepath = MEMORY_DIR / f"{slug}.md"
filepath.write_text(
f"---\nname: {name}\ndescription: {description}\ntype: {mem_type}\n---\n\n{body}\n"
)
_rebuild_index()
```
### 読み込み2 つのパス
**パス 1インデックスを SYSTEM に常駐。** `build_system()` は毎ターン SYSTEM を再構築する際に `MEMORY.md` を読み込み、記憶カタログを注入。SYSTEM prompt 内のインデックスは prompt cache でキャッシュ可能で、毎ターン再送不要。
**パス 2関連記憶をオンデマンド注入。** 各 LLM 呼び出し前、`load_memories()` は最近の会話と記憶カタログname + descriptionを LLM に軽量 side-query として送信し、関連するファイル名を選択、ファイル内容を読み込んで注入。上限 5 件でコストを制御。
```python
def select_relevant_memories(messages, max_items=5):
files = list_memory_files()
if not files:
return []
# Build catalog: "0: user-preference-tabs — User prefers tabs..."
catalog = "\n".join(f"{i}: {f['name']}{f['description']}" for i, f in enumerate(files))
response = client.messages.create(model=MODEL, messages=[{"role": "user",
"content": f"Select relevant memory indices. Return JSON array.\n\n"
f"Recent conversation:\n{recent}\n\nMemory catalog:\n{catalog}"}],
max_tokens=200)
indices = json.loads(re.search(r'\[.*?\]', response.content[0].text).group())
return [files[i]["filename"] for i in indices if 0 <= i < len(files)]
```
side-query が失敗した場合API エラー、JSON パース失敗、name + description のキーワードマッチにフォールバック。
### 書き込み:毎ターン終了後の抽出
ユーザーが毎回「これを覚えて」と言うわけではない。好みは通常、通常の会話の中に散らばっている:「タブの方がスペースより良い」「これからはシングルクォートにしよう」。
`extract_memories()` は各ターン終了時に実行、モデルが tool_use なしで停止した場合にトリガー(会話が自然な区切りに達したことを示す):
```python
# In agent_loop:
if response.stop_reason != "tool_use":
extract_memories(messages) # 最近の会話から新しい記憶を抽出
consolidate_memories() # 整理が必要かチェック
return
```
抽出前に既存の記憶を確認し、重複を回避。抽出プロンプトは LLM に `{name, type, description, body}` の JSON 配列を要求、本当に新しい情報がある場合のみファイルに書き込む。
```python
def extract_memories(messages):
dialogue = format_recent_messages(messages[-10:])
existing = "\n".join(f"- {m['name']}: {m['description']}" for m in list_memory_files())
prompt = (
"Extract user preferences, constraints, or project facts.\n"
"Return JSON array: [{name, type, description, body}].\n"
"If nothing new or already covered, return [].\n\n"
f"Existing memories:\n{existing}\n\nDialogue:\n{dialogue[:4000]}"
)
# ... parse response, write files ...
```
### 整理:低頻度の重複排除
記憶ファイルは蓄積される。`consolidate_memories()` はファイル数が閾値(デフォルト 10に達した時にトリガー、LLM に重複排除、矛盾の統合、古い記憶の剪定を依頼:
```python
CONSOLIDATE_THRESHOLD = 10
def consolidate_memories():
files = list_memory_files()
if len(files) < CONSOLIDATE_THRESHOLD:
return # 少なすぎる、整理する価値なし
# Send all memories to LLM, get back deduplicated list
# Replace all files with consolidated results
```
CC はこのプロセスを **Dream** と呼び、実際には 4 層のゲートがある:時間間隔、スキャンスロットル、セッション数、ファイルロック。教学版はファイル数閾値に簡略化。
### Memory に保存するもの
Memory はセッションを越えて有用な情報を保存する:ユーザーの好み、繰り返し出るフィードバック、プロジェクト背景、よく使う入口、調査の手がかりなど。「あとでまた使うもの」を対象にし、インデックス + オンデマンド読み込みで現在の会話に戻す。
session memory は 1 つのセッション内の連続性を扱うcompact 後も現在の会話に残すべき文脈を保持する。両者は役割が分かれている。Memory は長期知識を扱い、session memory は現在のセッションを compact 越しにつなぐ。
---
## s08 からの変更点
| コンポーネント | 変更前 (s08) | 変更後 (s09) |
|-----------|-------------|-------------|
| 記憶能力 | なし(圧縮後、好みはサマリと共に劣化) | ストレージ + 読み込み + 抽出 + 整理 |
| 新規関数 | — | write_memory_file, select_relevant_memories, load_memories, extract_memories, consolidate_memories |
| ストレージ | — | .memory/MEMORY.md インデックス + .memory/*.md ファイル |
| ツール | bash, read, write, edit, glob, todo_write, task, load_skill, compact (9) | bash, read_file, write_file, edit_file, glob, task (6) |
| ループ | 毎ターン圧縮のみ | 記憶注入 + 圧縮 + ターン終了後の抽出 + 定期整理 |
---
## 試してみよう
```sh
cd learn-claude-code
python s09_memory/code.py
```
以下のプロンプトを試してみてください(複数ターンに分けて入力し、記憶の蓄積と読み込みを観察):
1. `I prefer using tabs for indentation, not spaces. Remember that.`
2. `Create a Python file called test.py`Agent がタブを使用したか観察)
3. `What did I tell you about my preferences?`Agent が覚えているか観察)
4. `I also prefer single quotes over double quotes for strings.`
観察のポイント:各ターン終了後に `[Memory: extracted N new memories]` が表示されるか?`.memory/` ディレクトリに `.md` ファイルが生成されたか?`MEMORY.md` インデックスが更新されたか?新しい会話で Agent が以前の記憶を自動的に読み込んだか?
---
## 次へ
記憶、圧縮、ツールはすべて揃った。しかし system prompt はまだハードコードされた文字列。新しいツールを追加するには手動で説明を書き、プロジェクトを変えるにはプロンプト全体を書き直す。プロンプトは実行時に組み立てられるべき。
s10 System Prompt → セグメント + 実行時組み立て。異なるプロジェクト、異なるツール、異なるプロンプト。
<details>
<summary>CC ソースコードの詳細</summary>
> 以下は CC ソースコード `src/` 下の `memdir/`、`services/`、`utils/`、`query/` の分析に基づく。行番号はソースコードと照合済み。
### ソースコードパス
| ファイル | 行数 | 職責 |
|------|------|------|
| `memdir/memdir.ts` | 507 | 核心MEMORY.md 定義(`34-38`)、記憶動作指示で memory/plan/tasks を区別(`199-266`)、`loadMemoryPrompt()` 3 パス(`419-490` |
| `memdir/findRelevantMemories.ts` | 141 | Sonnet side-query で記憶選択(`18-24` システムプロンプト、`97-122` 呼び出しロジック) |
| `memdir/memoryTypes.ts` | 271 | 型定義、frontmatter フィールド |
| `memdir/memoryScan.ts` | — | .md ファイルをスキャン、MEMORY.md を除外、frontmatter を読み取り、最大 200 ファイル、mtime 降順(`35-94` |
| `services/extractMemories/extractMemories.ts` | 615 | forked agent で記憶を抽出、制限付き権限、`skipTranscript: true``maxTurns: 5``371-427` |
| `services/autoDream/autoDream.ts` | 324 | Dream 整理、4 層ゲート(`63-66` デフォルト値、`130-190` ゲート、`224-233` forked agent |
| `services/SessionMemory/sessionMemory.ts` | 495 | セッションレベルの記憶管理 |
| `services/compact/sessionMemoryCompact.ts` | — | session memory 軽量サマリ、閾値 10K/5/40K`56-61` |
| `utils/attachments.ts` | — | 注入予算200 行 / 4096 バイト/ファイル、60KB/セッション(`269-288`query で関連記憶を検索(`2196-2241` |
| `query.ts` | — | memory prefetch を毎ターン開始時に起動(`301-304`)、非ブロッキング収集(`1592-1614` |
| `query/stopHooks.ts` | — | stop hook fire-and-forget で抽出と Dream をトリガー(`141-155` |
### 記憶選択embedding ではなく LLM
CC は **Sonnet 自身で選択**`findRelevantMemories.ts`、embedding ベクトル類似度ではない:
1. `memoryScan.ts``.memory/` 下のすべての `.md` ファイルをスキャンMEMORY.md を除外)、最大 200 ファイル、mtime 降順
2. `name` + `description` をカタログとしてリスト化
3. Sonnet side-query に送信:「名前と説明から本当に有用な記憶を選択(最大 5 件)。不明ならスキップ。」
4. Sonnet が `{ selected_memories: ["file1.md", ...] }` を返却
5. 選択されたファイルの完全な内容を読み込み(≤ 200 行 / 4096 バイト/ファイル、注入。セッション総予算60KB
毎ターンのユーザー turn 開始時、`query.ts:301-304` が memory prefetch を起動(非同期);ツール実行後、`1592-1614` が非ブロッキングで結果を収集。
### 抽出タイミングstop hook、autoCompact 後ではない
トリガー位置(`stopHooks.ts:141-155``handleStopHooks()` 内で、fire-and-forget で抽出と Dream をトリガー。教学版は `stop_reason != "tool_use"` 分岐に抽出を配置、方向は一致。
CC の抽出は forked agent で実行(`extractMemories.ts:371-427`):制限付き権限、`skipTranscript: true``maxTurns: 5`。重複保護もある:メイン Agent が既に記憶ファイルを書き込んだ場合、抽出をスキップ。
### 記憶ファイル形式
CC は Markdown + YAML frontmatter を使用、教学版と一致。4 種類:`user``feedback``project``reference`
`memdir.ts:34-38` がインデックス制約を定義:`MEMORY.md` 最大 200 行 / 25KB。`memdir.ts:199-266` が記憶動作指示を構築、memory と plan と tasks を明確に区別。保存場所:`~/.claude/projects/<sanitized-git-root>/memory/`
### Dream4 層ゲート
「アイドル時にトリガー」や「数が足りたら統合」ではなく、4 層のゲート(`autoDream.ts`、デフォルト値 `63-66`、ゲートロジック `130-190`
1. **時間ゲート**:前回の統合から ≥ 24 時間
2. **スキャンスロットル**:頻繁なファイルシステムスキャンを回避
3. **セッションゲート**:前回の統合以降 ≥ 5 セッションの transcript が変更された
4. **ロックゲート**:他のプロセスが統合中でない(`.consolidate-lock` ファイル)
統合自体は forked agent で実行(`224-233`):定位 → 直近のシグナル収集 → 統合してファイル書き込み → 剪定してインデックス更新。ロックファイルの mtime が lastConsolidatedAt。クラッシュリカバリ1 時間後にロックが自動期限切れ。
### User Memory vs Session Memory
| | User Memory | Session Memory |
|---|---|---|
| 永続性 | セッション間 | 単一セッション |
| ストレージ | `memory/` 下の複数 .md ファイル | `session-memory/<id>/memory.md` |
| 注入先 | system prompt | compact サマリ |
| 目的 | セッション間の知識蓄積 | compact を越えたコンテキストの連続性 |
sessionMemoryCompacts08 で触れた仕組み)は Session Memory を活用autoCompact の前に session memory ファイルを読み込み、内容が十分であれば(≥ 10K token、≥ 5 テキストメッセージ、≤ 40K token、`sessionMemoryCompact.ts:56-61`、LLM を呼び出さずにサマリとして使用。
### 実際の実装が教学版より複雑な点
- **Feature flags**:記憶関連機能には複数の feature gate 層がある
- **Team memory**:チーム共有記憶、`loadMemoryPrompt()` に専用パスあり(教学版では未カバー)
- **KAIROS**:タイミング認識型の記憶抽出戦略、`loadMemoryPrompt()` の daily-log モード
- **Prompt cache**:記憶注入は prompt cache の TTL を考慮する必要があり、毎ターン system prompt の大部分を書き直すことを避ける
- **ファイルロック**:マルチプロセス時の並行制御
- **Memory prefetch**:非同期プレフェッチ、メインフローをブロックしない
### 教学版の簡略化は意図的
- LLM side-query → LLM side-query + キーワードフォールバック:教学版は LLM 選択を維持し、フォールバックパスを追加
- 記憶 JSON → Markdown + frontmatter教学版は CC と一致
- stop hook トリガー → `stop_reason != "tool_use"` 分岐:方向は一致
- 4 層ゲート → ファイル数閾値:教学版には transcript システムやマルチセッションの概念がない
- forked agent + 制限付き権限 → 直接呼び出し:教学版にはサブプロセス分離がない
</details>
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->

279
s09_memory/README.md Normal file
View File

@@ -0,0 +1,279 @@
# s09: Memory — 压缩会丢细节,要有一层不丢的
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
s01 → ... → s07 → s08 → `s09` → [s10](../s10_system_prompt/) → s11 → ... → s20
> *"压缩会丢细节, 要有一层不丢的"* — 文件仓库 + 索引 + 按需加载,跨压缩、跨会话。
>
> **Harness 层**: 记忆 — 跨压缩、跨会话的知识积累。
---
## 问题
s08 的 autoCompact 会把当前目标、剩余工作、用户约束写进摘要,但细节会丢失:"用 tab 缩进不要用空格"可能被简化成"用户有代码风格偏好"。而且新开一个会话,连摘要也没了。
LLM 没有持久状态,所有信息都在上下文窗口里。上下文满了要压缩,压缩就有损。需要一层不参与压缩、跨会话保留的存储。
---
## 解决方案
![Memory Overview](images/memory-overview.svg)
s08 的压缩管线保留,聚焦记忆。存储选文件系统:`.memory/` 目录下,每个记忆一个 `.md` 文件,带 YAML frontmatter`name` / `description` / `type`)。文件多了需要索引:`MEMORY.md` 一行一个链接,注入 SYSTEM。
关键设计:索引常驻 SYSTEM prompt可被 prompt cache 缓存),文件内容按需注入(按 filename/description 匹配当前对话,不破坏 cache。写入分两条路径用户显式说"记住",或者每轮结束后后台提取。文件积累多了,定期整理去重。
四类记忆,各有用途:
| 类型 | 回答什么 | 示例 |
|------|---------|------|
| user | 你是谁 | "用 tab 不用空格" |
| feedback | 怎么做事 | "别 mock 数据库" |
| project | 正在发生什么 | "auth 重写是合规驱动" |
| reference | 东西在哪找 | "pipeline bug 在 Linear INGEST" |
---
## 工作原理
![Memory Subsystems](images/memory-subsystems.svg)
### 存储Markdown 文件 + 索引
每个记忆是一个 `.md` 文件YAML frontmatter 记录元数据:
```markdown
---
name: user-preference-tabs
description: User prefers tabs for indentation
type: user
---
User prefers using tabs, not spaces, for indentation.
**Why:** Consistency with existing codebase conventions.
**How to apply:** Always use tabs when writing or editing files.
```
`MEMORY.md` 是索引,一行一个链接:
```markdown
- [user-preference-tabs](user-preference-tabs.md) — User prefers tabs for indentation
```
写入新记忆时自动重建索引:
```python
def write_memory_file(name, mem_type, description, body):
slug = name.lower().replace(" ", "-")
filepath = MEMORY_DIR / f"{slug}.md"
filepath.write_text(
f"---\nname: {name}\ndescription: {description}\ntype: {mem_type}\n---\n\n{body}\n"
)
_rebuild_index()
```
### 加载:两条路径
**路径一:索引常驻 SYSTEM。** `build_system()` 每轮重建 SYSTEM 时读取 `MEMORY.md`把记忆清单注入。SYSTEM prompt 中的索引可以被 prompt cache 缓存,不需要每轮重新发送。
**路径二:相关记忆按需注入。** 每轮调用前,`load_memories()` 把最近对话和记忆目录name + description一起发给 LLM 做一次轻量 side-query选出相关的文件名再读文件内容注入上下文。最多 5 条,控制开销。
```python
def select_relevant_memories(messages, max_items=5):
files = list_memory_files()
if not files:
return []
# Build catalog: "0: user-preference-tabs — User prefers tabs..."
catalog = "\n".join(f"{i}: {f['name']}{f['description']}" for i, f in enumerate(files))
response = client.messages.create(model=MODEL, messages=[{"role": "user",
"content": f"Select relevant memory indices. Return JSON array.\n\n"
f"Recent conversation:\n{recent}\n\nMemory catalog:\n{catalog}"}],
max_tokens=200)
indices = json.loads(re.search(r'\[.*?\]', response.content[0].text).group())
return [files[i]["filename"] for i in indices if 0 <= i < len(files)]
```
如果 side-query 失败API 错误、JSON 解析失败),降级到关键词匹配 name + description。
### 写入:每轮结束后提取
用户不会每次都说"记住这个"。偏好通常散落在正常对话中:"用 tab 比空格好"、"以后都用单引号"。
`extract_memories()` 在每轮结束时运行,条件是模型停止且没有 tool_use说明对话告一段落
```python
# In agent_loop:
if response.stop_reason != "tool_use":
extract_memories(messages) # 从最近对话提取新记忆
consolidate_memories() # 检查是否需要整理
return
```
提取前先检查已有记忆,避免重复。提取 prompt 要求 LLM 返回 `{name, type, description, body}` 的 JSON 数组,只有确实有新信息时才写文件。
```python
def extract_memories(messages):
dialogue = format_recent_messages(messages[-10:])
existing = "\n".join(f"- {m['name']}: {m['description']}" for m in list_memory_files())
prompt = (
"Extract user preferences, constraints, or project facts.\n"
"Return JSON array: [{name, type, description, body}].\n"
"If nothing new or already covered, return [].\n\n"
f"Existing memories:\n{existing}\n\nDialogue:\n{dialogue[:4000]}"
)
# ... parse response, write files ...
```
### 整理:低频合并去重
记忆文件会积累。`consolidate_memories()` 在文件数达到阈值(默认 10时触发让 LLM 去重、合并矛盾、淘汰过时记忆:
```python
CONSOLIDATE_THRESHOLD = 10
def consolidate_memories():
files = list_memory_files()
if len(files) < CONSOLIDATE_THRESHOLD:
return # 太少,不值得整理
# Send all memories to LLM, get back deduplicated list
# Replace all files with consolidated results
```
CC 把这个过程叫 Dream实际有四层门控时间间隔、扫描节流、会话数、文件锁。教学版简化为文件数阈值。
### Memory 适合保存什么
Memory 保存跨会话仍然有用的信息:用户偏好、反复出现的反馈、项目背景、常用入口和排查线索。它关注“以后还会用到什么”,并通过索引 + 按需加载把这些信息带回当前对话。
session memory 关注同一会话内的连续性compact 之后当前会话还需要保留哪些上下文。两者配合使用Memory 管长期知识session memory 管当前会话的压缩续接。
---
## 相对 s08 的变更
| 组件 | 之前 (s08) | 之后 (s09) |
|------|-----------|-----------|
| 记忆能力 | 无(压缩后偏好随摘要退化) | 存储 + 加载 + 提取 + 整理 |
| 新函数 | — | write_memory_file, select_relevant_memories, load_memories, extract_memories, consolidate_memories |
| 存储 | — | .memory/MEMORY.md 索引 + .memory/*.md 文件 |
| 工具 | bash, read, write, edit, glob, todo_write, task, load_skill, compact (9) | bash, read_file, write_file, edit_file, glob, task (6) |
| 循环 | 每轮只做压缩 | 每轮注入记忆 + 压缩 + 每轮结束后提取 + 定期整理 |
---
## 试一下
```sh
cd learn-claude-code
python s09_memory/code.py
```
试试这些 prompt分多轮输入观察记忆的累积和加载
1. `I prefer using tabs for indentation, not spaces. Remember that.`
2. `Create a Python file called test.py`(观察 Agent 是否用了 tab
3. `What did I tell you about my preferences?`(观察 Agent 是否记得)
4. `I also prefer single quotes over double quotes for strings.`
观察重点:每轮结束后是否出现 `[Memory: extracted N new memories]``.memory/` 目录下是否生成了 `.md` 文件?`MEMORY.md` 索引是否更新?新一轮对话时 Agent 是否自动加载了之前的记忆?
---
## 接下来
记忆、压缩、工具都已就绪。但 system prompt 还是硬编码的一大段字符串。加了新工具要手动加描述,换了项目要重写整个 prompt。prompt 应该运行时组装。
s10 System Prompt → 分段 + 运行时组装。不同项目、不同工具,拼出不同的 prompt。
<details>
<summary>深入 CC 源码</summary>
> 以下基于 CC 源码 `src/` 下 `memdir/`、`services/`、`utils/`、`query/` 的分析,行号已对照核实。
### 源码路径
| 文件 | 行数 | 职责 |
|------|------|------|
| `memdir/memdir.ts` | 507 | 核心MEMORY.md 定义(`34-38`)、记忆行为指令区分 memory/plan/tasks`199-266`)、`loadMemoryPrompt()` 三条路径(`419-490` |
| `memdir/findRelevantMemories.ts` | 141 | Sonnet side-query 选记忆(`18-24` 系统提示、`97-122` 调用逻辑) |
| `memdir/memoryTypes.ts` | 271 | 类型定义frontmatter 字段 |
| `memdir/memoryScan.ts` | — | 扫描 .md 文件,排除 MEMORY.md读 frontmatter最多 200 个,按 mtime 降序(`35-94` |
| `services/extractMemories/extractMemories.ts` | 615 | forked agent 提取记忆,受限权限,`skipTranscript: true``maxTurns: 5``371-427` |
| `services/autoDream/autoDream.ts` | 324 | Dream 整理,四层门控(`63-66` 默认值、`130-190` 门控、`224-233` forked agent |
| `services/SessionMemory/sessionMemory.ts` | 495 | 会话级记忆管理 |
| `services/compact/sessionMemoryCompact.ts` | — | session memory 轻量摘要,阈值 10K/5/40K`56-61` |
| `utils/attachments.ts` | — | 注入预算200 行 / 4096 字节每文件60KB 每 session`269-288`);按 query 找相关 memory`2196-2241` |
| `query.ts` | — | memory prefetch 每轮启动(`301-304`),非阻塞收集(`1592-1614` |
| `query/stopHooks.ts` | — | stop hook fire-and-forget 触发提取和 Dream`141-155` |
### 记忆选择LLM 选,不是 embedding
CC 用 **Sonnet 本身来选**`findRelevantMemories.ts`),不是 embedding 向量相似度:
1. `memoryScan.ts` 扫描 `.memory/` 下所有 `.md` 文件(排除 MEMORY.md最多 200 个,按 mtime 降序
2.`name` + `description` 列成清单
3. 发给 Sonnet side-query"根据名称和描述选出真正有用的记忆(最多 5 个)。不确定就不要选。"
4. Sonnet 返回 `{ selected_memories: ["file1.md", ...] }`
5. 选中文件读取完整内容(每文件 ≤ 200 行 / 4096 字节),注入上下文。单 session 总预算 60KB
每轮用户 turn 开始时,`query.ts:301-304` 启动 memory prefetch异步工具执行后 `1592-1614` 非阻塞收集结果,不卡主流程。
### 提取时机stop hook不是 autoCompact 后
触发位置(`stopHooks.ts:141-155`):在 `handleStopHooks()`fire-and-forget 触发提取和 Dream。教学版把提取放在 `stop_reason != "tool_use"` 分支里,方向一致。
CC 的提取通过 forked agent 执行(`extractMemories.ts:371-427`):受限权限、`skipTranscript: true``maxTurns: 5`。还有重叠保护:如果主 Agent 已经写入了记忆文件,跳过提取。
### 记忆文件格式
CC 用 Markdown + YAML frontmatter和教学版一致。四种类型`user``feedback``project``reference`
`memdir.ts:34-38` 定义索引约束:`MEMORY.md` 最多 200 行 / 25KB。`memdir.ts:199-266` 构建记忆行为指令,明确区分 memory、plan、tasks。存储位置`~/.claude/projects/<sanitized-git-root>/memory/`
### Dream四层门控
不是"空闲时触发"或"数量够了就合并",而是四层门控(`autoDream.ts`,默认值 `63-66`,门控逻辑 `130-190`
1. **时间门控**:距上次合并 ≥ 24 小时
2. **扫描节流**:避免频繁扫描文件系统
3. **会话门控**:自上次合并以来修改了 ≥ 5 个会话 transcript
4. **锁门控**:没有其他进程正在合并(`.consolidate-lock` 文件)
合并本身通过 forked agent 执行(`224-233`):定位 → 收集近期信号 → 合并写文件 → 剪枝更新索引。锁文件 mtime 就是 lastConsolidatedAt。崩溃恢复1 小时后锁自动过期。
### User Memory vs Session Memory
| | User Memory | Session Memory |
|---|---|---|
| 持久性 | 跨会话 | 单会话 |
| 存储 | `memory/` 下多个 .md 文件 | `session-memory/<id>/memory.md` |
| 加载到 | system prompt | compact 摘要 |
| 用途 | 跨会话的知识积累 | 跨 compact 的上下文连续性 |
sessionMemoryCompacts08 中提到的机制)正是使用了 Session MemoryautoCompact 前先读 session memory 文件,如果内容足够(≥ 10K token、≥ 5 条文本消息、≤ 40K token`sessionMemoryCompact.ts:56-61`),就用它做摘要,不调 LLM。
### 真实实现比教学版复杂的地方
- **Feature flags**:记忆相关功能有多层 feature gate 控制
- **Team memory**:团队共享记忆,`loadMemoryPrompt()` 有专门路径(教学版未涉及)
- **KAIROS**:时机感知的记忆提取策略,`loadMemoryPrompt()` 中 daily-log 模式
- **Prompt cache**:记忆注入需要考虑 prompt cache 的 TTL避免每次都重写 system prompt 的大段内容
- **文件锁**:多进程并发时的锁机制
- **Memory prefetch**:异步预取,不阻塞主流程
### 教学版的简化是刻意的
- LLM side-query → LLM side-query + 关键词降级:教学版保留了 LLM 选择,加了降级路径
- 记忆 JSON → Markdown + frontmatter教学版与 CC 一致
- stop hook 触发 → `stop_reason != "tool_use"` 分支:方向一致
- 四层门控 → 文件数阈值:教学版没有 transcript 系统和多会话概念
- forked agent + 受限权限 → 直接调用:教学版没有子进程隔离
</details>
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->

616
s09_memory/code.py Normal file
View File

@@ -0,0 +1,616 @@
#!/usr/bin/env python3
"""
s09_memory.py - Memory System
Persistent, cross-session knowledge for the coding agent.
Storage:
.memory/
MEMORY.md ← index (one line per memory, ≤200 lines)
feedback_tabs.md ← individual memory files (Markdown + YAML frontmatter)
user_profile.md
project_facts.md
Flow in agent_loop:
1. Load MEMORY.md index into SYSTEM prompt (cheap, always present)
2. Select relevant memories by filename/description → inject content
3. Run compression pipeline from s08
4. After each turn ends → extract new memories from original messages
5. Periodically consolidate (Dream)
Builds on s08 (context compact). Usage:
python s09_memory/code.py
Needs: pip install anthropic python-dotenv + ANTHROPIC_API_KEY in .env
"""
import os, subprocess, json, time, re
from pathlib import Path
try:
import readline
readline.parse_and_bind('set bind-tty-special-chars off')
except ImportError:
pass
from anthropic import Anthropic
from dotenv import load_dotenv
load_dotenv(override=True)
if os.getenv("ANTHROPIC_BASE_URL"): os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
WORKDIR = Path.cwd()
MEMORY_DIR = WORKDIR / ".memory"; MEMORY_DIR.mkdir(exist_ok=True)
MEMORY_INDEX = MEMORY_DIR / "MEMORY.md"
SKILLS_DIR = WORKDIR / "skills"
TRANSCRIPT_DIR = WORKDIR / ".transcripts"
TOOL_RESULTS_DIR = WORKDIR / ".task_outputs" / "tool-results"
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
MODEL = os.environ["MODEL_ID"]
# ═══════════════════════════════════════════════════════════
# NEW in s09: Memory System
# ═══════════════════════════════════════════════════════════
MEMORY_TYPES = ["user", "feedback", "project", "reference"]
def _parse_frontmatter(text: str) -> tuple[dict, str]:
if not text.startswith("---"):
return {}, text
parts = text.split("---", 2)
if len(parts) < 3:
return {}, text
meta = {}
for line in parts[1].strip().splitlines():
if ":" in line:
k, v = line.split(":", 1)
meta[k.strip()] = v.strip().strip('"').strip("'")
return meta, parts[2].strip()
def write_memory_file(name: str, mem_type: str, description: str, body: str):
"""Write a single memory file with YAML frontmatter."""
slug = name.lower().replace(" ", "-").replace("/", "-")
filename = f"{slug}.md"
filepath = MEMORY_DIR / filename
filepath.write_text(
f"---\nname: {name}\ndescription: {description}\ntype: {mem_type}\n---\n\n{body}\n"
)
_rebuild_index()
return filepath
def _rebuild_index():
"""Rebuild MEMORY.md index from all memory files."""
lines = []
for f in sorted(MEMORY_DIR.glob("*.md")):
if f.name == "MEMORY.md":
continue
raw = f.read_text()
meta, body = _parse_frontmatter(raw)
name = meta.get("name", f.stem)
desc = meta.get("description", body.split("\n")[0][:80])
lines.append(f"- [{name}]({f.name}) — {desc}")
MEMORY_INDEX.write_text("\n".join(lines) + "\n" if lines else "")
def read_memory_index() -> str:
"""Read MEMORY.md index (injected into SYSTEM every turn)."""
if not MEMORY_INDEX.exists():
return ""
text = MEMORY_INDEX.read_text().strip()
return text if text else ""
def read_memory_file(filename: str) -> str | None:
"""Read a single memory file's full content."""
path = MEMORY_DIR / filename
if not path.exists():
return None
return path.read_text()
def list_memory_files() -> list[dict]:
"""List all memory files with metadata."""
result = []
for f in sorted(MEMORY_DIR.glob("*.md")):
if f.name == "MEMORY.md":
continue
raw = f.read_text()
meta, body = _parse_frontmatter(raw)
result.append({
"filename": f.name,
"name": meta.get("name", f.stem),
"description": meta.get("description", ""),
"type": meta.get("type", "user"),
"body": body,
})
return result
def select_relevant_memories(messages: list, max_items: int = 5) -> list[str]:
"""Select relevant memory filenames by matching recent conversation against
memory names/descriptions. Uses a simple LLM call (or falls back to keyword
matching on name+description)."""
files = list_memory_files()
if not files:
return []
# Collect recent user text for context
recent_texts = []
for msg in reversed(messages):
if msg.get("role") == "user":
content = msg.get("content", "")
if isinstance(content, list):
content = " ".join(
str(getattr(b, "text", "")) for b in content
if getattr(b, "type", None) == "text"
)
if isinstance(content, str):
recent_texts.append(content)
if len(recent_texts) >= 3:
break
recent = " ".join(reversed(recent_texts))[:2000]
if not recent.strip():
return []
# Build catalog of name + description for LLM to choose from
catalog_lines = []
for i, f in enumerate(files):
catalog_lines.append(f"{i}: {f['name']}{f['description']}")
catalog = "\n".join(catalog_lines)
prompt = (
"Given the recent conversation and the memory catalog below, "
"select the indices of memories that are clearly relevant. "
"Return ONLY a JSON array of integers, e.g. [0, 3]. "
"If none are relevant, return [].\n\n"
f"Recent conversation:\n{recent}\n\n"
f"Memory catalog:\n{catalog}"
)
try:
response = client.messages.create(
model=MODEL,
messages=[{"role": "user", "content": prompt}],
max_tokens=200,
)
text = response.content[0].text.strip()
# Extract JSON array from response
match = re.search(r'\[.*?\]', text, re.DOTALL)
if match:
indices = json.loads(match.group())
selected = []
for idx in indices:
if isinstance(idx, int) and 0 <= idx < len(files):
selected.append(files[idx]["filename"])
if len(selected) >= max_items:
break
return selected
except Exception:
pass
# Fallback: keyword matching on name + description
keywords = [w.lower() for w in recent.split() if len(w) > 3]
selected = []
for f in files:
text = (f["name"] + " " + f["description"]).lower()
if any(kw in text for kw in keywords):
selected.append(f["filename"])
if len(selected) >= max_items:
break
return selected
def load_memories(messages: list) -> str:
"""Load relevant memory content for injection into context."""
selected_files = select_relevant_memories(messages)
if not selected_files:
return ""
parts = ["<relevant_memories>"]
for filename in selected_files:
content = read_memory_file(filename)
if content:
parts.append(content)
parts.append("</relevant_memories>")
return "\n\n".join(parts)
def extract_memories(messages: list):
"""Extract new memories from recent dialogue. Runs after each turn."""
# Collect recent conversation text
dialogue_parts = []
for msg in messages[-10:]:
role = msg.get("role", "?")
content = msg.get("content", "")
if isinstance(content, list):
content = " ".join(
str(getattr(b, "text", "")) for b in content
if getattr(b, "type", None) == "text"
)
if isinstance(content, str) and content.strip():
dialogue_parts.append(f"{role}: {content}")
dialogue = "\n".join(dialogue_parts)
if not dialogue.strip():
return
# Check existing memories to avoid duplicates
existing = list_memory_files()
existing_desc = "\n".join(f"- {m['name']}: {m['description']}" for m in existing) if existing else "(none)"
prompt = (
"Extract user preferences, constraints, or project facts from this dialogue.\n"
"Return a JSON array. Each item: {name, type, description, body}.\n"
"- name: short kebab-case identifier (e.g. 'user-preference-tabs')\n"
"- type: one of 'user' (user preference), 'feedback' (guidance), "
"'project' (project fact), 'reference' (external pointer)\n"
"- description: one-line summary for index lookup\n"
"- body: full detail in markdown\n"
"If nothing new or already covered by existing memories, return [].\n\n"
f"Existing memories:\n{existing_desc}\n\n"
f"Dialogue:\n{dialogue[:4000]}"
)
try:
response = client.messages.create(
model=MODEL, messages=[{"role": "user", "content": prompt}], max_tokens=800
)
text = response.content[0].text.strip()
# Extract JSON array from response
match = re.search(r'\[.*\]', text, re.DOTALL)
if not match:
return
items = json.loads(match.group())
if not items:
return
count = 0
for mem in items:
name = mem.get("name", f"memory_{int(time.time())}")
mem_type = mem.get("type", "user")
desc = mem.get("description", "")
body = mem.get("body", "")
if desc and body:
write_memory_file(name, mem_type, desc, body)
count += 1
if count:
print(f"\n\033[33m[Memory: extracted {count} new memories]\033[0m")
except Exception:
pass
CONSOLIDATE_THRESHOLD = 10
def consolidate_memories():
"""Merge duplicate/stale memories. Triggered when file count ≥ threshold."""
files = list_memory_files()
if len(files) < CONSOLIDATE_THRESHOLD:
return
catalog = "\n\n".join(
f"## {f['filename']}\nname: {f['name']}\ndescription: {f['description']}\n{f['body']}"
for f in files
)
prompt = (
"Consolidate the following memory files. Rules:\n"
"1. Merge duplicates into one\n"
"2. Remove outdated/contradicted memories\n"
"3. Keep the total under 30 memories\n"
"4. Preserve important user preferences above all\n"
"Return a JSON array. Each item: {name, type, description, body}.\n\n"
f"{catalog[:16000]}"
)
try:
response = client.messages.create(
model=MODEL, messages=[{"role": "user", "content": prompt}], max_tokens=3000
)
text = response.content[0].text.strip()
match = re.search(r'\[.*\]', text, re.DOTALL)
if not match:
return
items = json.loads(match.group())
# Remove old memory files (keep MEMORY.md)
for f in MEMORY_DIR.glob("*.md"):
if f.name != "MEMORY.md":
f.unlink()
for mem in items:
name = mem.get("name", f"memory_{int(time.time())}")
mem_type = mem.get("type", "user")
desc = mem.get("description", "")
body = mem.get("body", "")
if desc and body:
write_memory_file(name, mem_type, desc, body)
print(f"\n\033[33m[Memory: consolidated {len(files)}{len(items)} memories]\033[0m")
except Exception:
pass
# Build SYSTEM with memory index
def build_system() -> str:
index = read_memory_index()
memories_section = f"\n\nMemories available:\n{index}" if index else ""
return (
f"You are a coding agent at {WORKDIR}."
f"{memories_section}\n"
"Relevant memories are injected below. Respect user preferences from memory.\n"
"When the user says 'remember' or expresses a clear preference, extract it as a memory."
)
SYSTEM = build_system()
SUB_SYSTEM = (
f"You are a coding agent at {WORKDIR}. "
"Complete the task you were given, then return a concise summary. "
"Do not delegate further."
)
# ═══════════════════════════════════════════════════════════
# FROM s02-s08 (skeleton): Basic tools
# ═══════════════════════════════════════════════════════════
def safe_path(p: str) -> Path:
path = (WORKDIR / p).resolve()
if not path.is_relative_to(WORKDIR): raise ValueError(f"Path escapes workspace: {p}")
return path
def run_bash(command: str) -> str:
try:
r = subprocess.run(command, shell=True, cwd=WORKDIR, capture_output=True, text=True, timeout=120)
out = (r.stdout + r.stderr).strip()
return out[:50000] if out else "(no output)"
except subprocess.TimeoutExpired: return "Error: Timeout (120s)"
def run_read(path: str, limit: int | None = None) -> str:
try:
lines = safe_path(path).read_text().splitlines()
if limit and limit < len(lines): lines = lines[:limit] + [f"... ({len(lines) - limit} more lines)"]
return "\n".join(lines)
except Exception as e: return f"Error: {e}"
def run_write(path: str, content: str) -> str:
try:
file_path = safe_path(path); file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text(content); return f"Wrote {len(content)} bytes to {path}"
except Exception as e: return f"Error: {e}"
def run_edit(path: str, old_text: str, new_text: str) -> str:
try:
file_path = safe_path(path)
text = file_path.read_text()
if old_text not in text: return f"Error: text not found in {path}"
file_path.write_text(text.replace(old_text, new_text, 1))
return f"Edited {path}"
except Exception as e: return f"Error: {e}"
def run_glob(pattern: str) -> str:
import glob as g
try:
results = []
for match in g.glob(pattern, root_dir=WORKDIR):
if (WORKDIR / match).resolve().is_relative_to(WORKDIR):
results.append(match)
return "\n".join(results) if results else "(no matches)"
except Exception as e: return f"Error: {e}"
def extract_text(content) -> str:
if not isinstance(content, list): return str(content)
return "\n".join(getattr(b, "text", "") for b in content if getattr(b, "type", None) == "text")
# Subagent (simplified from s06-s07)
SUB_TOOLS = [
{"name": "bash", "description": "Run a shell command.",
"input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}},
{"name": "read_file", "description": "Read file contents.",
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}},
{"name": "write_file", "description": "Write content to a file.",
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
]
SUB_HANDLERS = {"bash": run_bash, "read_file": run_read, "write_file": run_write}
def spawn_subagent(task: str) -> str:
print(f"\n\033[35m[Subagent spawned]\033[0m")
messages = [{"role": "user", "content": task}]
for _ in range(30):
response = client.messages.create(model=MODEL, system=SUB_SYSTEM,
messages=messages, tools=SUB_TOOLS, max_tokens=8000)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use": break
results = []
for block in response.content:
if block.type == "tool_use":
handler = SUB_HANDLERS.get(block.name)
output = handler(**block.input) if handler else f"Unknown: {block.name}"
print(f" \033[90m[sub] {block.name}: {str(output)[:100]}\033[0m")
results.append({"type": "tool_result", "tool_use_id": block.id, "content": output})
messages.append({"role": "user", "content": results})
result = extract_text(messages[-1]["content"])
if not result:
for msg in reversed(messages):
if msg["role"] == "assistant":
result = extract_text(msg["content"])
if result: break
if not result: result = "Subagent stopped after 30 turns without final answer."
print(f"\033[35m[Subagent done]\033[0m")
return result
# ═══════════════════════════════════════════════════════════
# FROM s08 (skeleton): Compaction pipeline
# ═══════════════════════════════════════════════════════════
CONTEXT_LIMIT = 50000; KEEP_RECENT = 3; PERSIST_THRESHOLD = 30000
def estimate_size(msgs): return len(str(msgs))
def snip_compact(msgs, mx=50):
if len(msgs) <= mx: return msgs
return msgs[:3] + [{"role": "user", "content": f"[snipped {len(msgs)-mx} msgs]"}] + msgs[-(mx-3):]
def collect_tool_results(msgs):
blocks = []
for mi, msg in enumerate(msgs):
if msg.get("role") != "user" or not isinstance(msg.get("content"), list): continue
for bi, block in enumerate(msg["content"]):
if isinstance(block, dict) and block.get("type") == "tool_result": blocks.append((mi, bi, block))
return blocks
def micro_compact(msgs):
tr = collect_tool_results(msgs)
if len(tr) <= KEEP_RECENT: return msgs
for _, _, b in tr[:-KEEP_RECENT]:
if len(b.get("content", "")) > 120: b["content"] = "[Earlier tool result compacted.]"
return msgs
def persist_large(tid, out):
if len(out) <= PERSIST_THRESHOLD: return out
TOOL_RESULTS_DIR.mkdir(parents=True, exist_ok=True)
p = TOOL_RESULTS_DIR / f"{tid}.txt"
if not p.exists(): p.write_text(out)
return f"<persisted-output>\nFull: {p}\nPreview:\n{out[:2000]}\n</persisted-output>"
def tool_result_budget(msgs, mx=200_000):
last = msgs[-1] if msgs else None
if not last or last.get("role") != "user" or not isinstance(last.get("content"), list): return msgs
blocks = [(i, b) for i, b in enumerate(last["content"]) if isinstance(b, dict) and b.get("type") == "tool_result"]
total = sum(len(str(b.get("content", ""))) for _, b in blocks)
if total <= mx: return msgs
for _, block in sorted(blocks, key=lambda p: len(str(p[1].get("content", ""))), reverse=True):
if total <= mx: break
c = str(block.get("content", ""))
if len(c) <= PERSIST_THRESHOLD: continue
block["content"] = persist_large(block.get("tool_use_id", "?"), c)
total = sum(len(str(b.get("content", ""))) for _, b in blocks)
return msgs
def write_transcript(msgs):
TRANSCRIPT_DIR.mkdir(parents=True, exist_ok=True)
p = TRANSCRIPT_DIR / f"transcript_{int(time.time())}.jsonl"
with p.open("w") as f:
for m in msgs: f.write(json.dumps(m, default=str) + "\n")
return p
def summarize_history(msgs):
conv = json.dumps(msgs, default=str)[:80000]
r = client.messages.create(model=MODEL, messages=[{"role": "user", "content":
"Summarize this coding-agent conversation so work can continue.\n"
"Preserve: 1. current goal, 2. key findings, 3. files changed, 4. remaining work, 5. user constraints.\n\n" + conv}],
max_tokens=2000)
return r.content[0].text.strip()
def compact_history(msgs):
write_transcript(msgs)
summary = summarize_history(msgs)
return [{"role": "user", "content": f"[Compacted]\n\n{summary}"}]
def reactive_compact(msgs):
write_transcript(msgs)
summary = summarize_history(msgs)
return [{"role": "user", "content": f"[Reactive compact]\n\n{summary}"}, *msgs[-5:]]
# ═══════════════════════════════════════════════════════════
# Tool Definitions (skeleton — fewer tools to focus on memory)
# ═══════════════════════════════════════════════════════════
TOOLS = [
{"name": "bash", "description": "Run a shell command.",
"input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}},
{"name": "read_file", "description": "Read file contents.",
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}},
{"name": "write_file", "description": "Write content to a file.",
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
{"name": "edit_file", "description": "Replace exact text in a file once.",
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}},
{"name": "glob", "description": "Find files matching a glob pattern.",
"input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
{"name": "task", "description": "Launch a subagent to handle a subtask.",
"input_schema": {"type": "object", "properties": {"description": {"type": "string"}}, "required": ["description"]}},
]
TOOL_HANDLERS = {
"bash": run_bash, "read_file": run_read, "write_file": run_write,
"edit_file": run_edit, "glob": run_glob, "task": spawn_subagent,
}
# ═══════════════════════════════════════════════════════════
# agent_loop — s09: inject memories + extract after each turn
# ═══════════════════════════════════════════════════════════
MAX_REACTIVE_RETRIES = 1
def agent_loop(messages: list):
reactive_retries = 0
while True:
# s09: rebuild system with current memory index + relevant memories
system = build_system()
memories_content = load_memories(messages)
if memories_content:
system += "\n\n" + memories_content
# s09: save pre-compression snapshot for accurate memory extraction
pre_compress = [m if isinstance(m, dict) else {"role": m.get("role",""),
"content": str(m.get("content",""))} for m in messages]
# s08: compression pipeline (budget → snip → micro)
messages[:] = tool_result_budget(messages)
messages[:] = snip_compact(messages)
messages[:] = micro_compact(messages)
if estimate_size(messages) > CONTEXT_LIMIT:
print("[auto compact]")
messages[:] = compact_history(messages)
try:
response = client.messages.create(
model=MODEL, system=system, messages=messages, tools=TOOLS, max_tokens=8000
)
reactive_retries = 0
except Exception as e:
if ("prompt_too_long" in str(e).lower() or "too many tokens" in str(e).lower()) and reactive_retries < MAX_REACTIVE_RETRIES:
print("[reactive compact]")
messages[:] = reactive_compact(messages)
reactive_retries += 1
continue
raise
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
# s09: extract from pre-compression snapshot for full fidelity
extract_memories(pre_compress)
consolidate_memories()
return
results = []
for block in response.content:
if block.type != "tool_use": continue
print(f"\033[36m> {block.name}\033[0m")
handler = TOOL_HANDLERS.get(block.name)
output = handler(**block.input) if handler else f"Unknown: {block.name}"
print(str(output)[:200])
results.append({"type": "tool_result", "tool_use_id": block.id, "content": output})
messages.append({"role": "user", "content": results})
if __name__ == "__main__":
print("s09: Memory — persistent cross-session knowledge")
print("输入问题,回车发送。输入 q 退出。\n")
history = []
while True:
try: query = input("\033[36ms09 >> \033[0m")
except (EOFError, KeyboardInterrupt): break
if query.strip().lower() in ("q", "exit", ""): break
history.append({"role": "user", "content": query})
agent_loop(history)
for block in history[-1]["content"]:
if getattr(block, "type", None) == "text": print(block.text)
print()

View File

@@ -0,0 +1,104 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 430" font-family="system-ui, -apple-system, sans-serif">
<defs>
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#7c3aed"/>
</linearGradient>
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
</marker>
<marker id="arrow-purple" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#7c3aed"/>
</marker>
<marker id="arrow-green" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#16a34a"/>
</marker>
</defs>
<rect width="760" height="430" fill="#fafbfc" rx="8"/>
<!-- Title -->
<rect x="0" y="0" width="760" height="44" fill="url(#header)" rx="8"/>
<rect x="0" y="36" width="760" height="8" fill="url(#header)"/>
<text x="380" y="28" fill="#fff" font-size="15" font-weight="700" text-anchor="middle">Memory — Memory loading, extraction, and consolidation on s08 compression pipeline</text>
<!-- Legend -->
<rect x="40" y="56" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="58" y="66" fill="#2563eb" font-size="10" font-weight="600">s08 preserved</text>
<rect x="160" y="56" width="12" height="10" rx="2" fill="#f3e8ff" stroke="#7c3aed" stroke-width="1"/>
<text x="178" y="66" fill="#7c3aed" font-size="10" font-weight="600">s09 new</text>
<!-- ===== messages[] ===== -->
<rect x="30" y="96" width="100" height="52" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="80" y="126" fill="#1e3a5f" font-size="12" font-weight="600" text-anchor="middle">messages[]</text>
<!-- arrow → compression -->
<line x1="130" y1="122" x2="152" y2="122" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- ===== Compression pipeline (s08) ===== -->
<rect x="155" y="86" width="135" height="72" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="222" y="108" fill="#1e3a5f" font-size="11" font-weight="700" text-anchor="middle">Compression</text>
<text x="222" y="124" fill="#64748b" font-size="9" text-anchor="middle">budget → snip → micro</text>
<text x="222" y="138" fill="#64748b" font-size="9" text-anchor="middle">→ autoCompact</text>
<text x="222" y="152" fill="#94a3b8" font-size="8" text-anchor="middle">(s08)</text>
<!-- arrow → Loading (purple) -->
<line x1="290" y1="122" x2="317" y2="122" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow-purple)"/>
<!-- ===== Loading (s09) ===== -->
<rect x="320" y="86" width="120" height="72" rx="8" fill="#f3e8ff" stroke="#7c3aed" stroke-width="2"/>
<text x="380" y="108" fill="#5b21b6" font-size="11" font-weight="700" text-anchor="middle">Loading</text>
<text x="380" y="124" fill="#7c3aed" font-size="9" text-anchor="middle">LLM side-query select</text>
<text x="380" y="138" fill="#7c3aed" font-size="9" text-anchor="middle">inject file contents</text>
<text x="380" y="152" fill="#a78bfa" font-size="8" text-anchor="middle">≤ 5 items</text>
<!-- arrow → LLM -->
<line x1="440" y1="122" x2="472" y2="122" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- ===== LLM (s08) ===== -->
<rect x="475" y="96" width="80" height="52" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="515" y="114" fill="#1e3a5f" font-size="14" font-weight="700" text-anchor="middle">LLM</text>
<text x="515" y="132" fill="#64748b" font-size="9" text-anchor="middle">stop_reason</text>
<text x="515" y="144" fill="#64748b" font-size="9" text-anchor="middle">=tool_use?</text>
<!-- LLM → no → return result -->
<line x1="515" y1="148" x2="515" y2="178" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
<text x="528" y="168" fill="#16a34a" font-size="9" font-weight="600">no, stop</text>
<rect x="460" y="180" width="110" height="24" rx="12" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
<text x="515" y="196" fill="#166534" font-size="10" font-weight="600" text-anchor="middle">return result</text>
<!-- LLM → yes → TOOL_HANDLERS -->
<line x1="555" y1="122" x2="587" y2="122" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<text x="568" y="114" fill="#64748b" font-size="9" font-weight="600">yes</text>
<!-- ===== TOOL_HANDLERS (s08) ===== -->
<rect x="590" y="88" width="130" height="68" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="655" y="112" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">TOOL_HANDLERS</text>
<text x="655" y="128" fill="#64748b" font-size="9" text-anchor="middle">bash · read · write</text>
<text x="655" y="142" fill="#94a3b8" font-size="8" text-anchor="middle">edit · glob · task</text>
<!-- ===== Memory Files (s09) ===== -->
<rect x="155" y="232" width="430" height="36" rx="6" fill="#faf5ff" stroke="#7c3aed" stroke-width="1.5" stroke-dasharray="4,2"/>
<text x="370" y="255" fill="#5b21b6" font-size="11" font-weight="600" text-anchor="middle">.memory/ — MEMORY.md index + *.md files (cross-session persistent)</text>
<!-- Arrow: Memory Files → Loading -->
<path d="M 395 232 L 395 162" fill="none" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow-purple)"/>
<text x="408" y="200" fill="#7c3aed" font-size="9">read</text>
<!-- Arrow: return result → Extraction → Memory Files -->
<path d="M 515 204 L 515 232" fill="none" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow-purple)"/>
<text x="528" y="222" fill="#7c3aed" font-size="9">Extraction (after each turn)</text>
<!-- Consolidation note -->
<text x="222" y="284" fill="#a78bfa" font-size="9">Consolidation: triggers at ≥ 10 files, dedup·merge·prune</text>
<!-- ===== Loop back ===== -->
<path d="M 720 122 L 748 122 Q 756 122 756 130 L 756 310 Q 756 318 748 318 L 88 318 Q 80 318 80 310 L 80 148" fill="none" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
<text x="400" y="340" fill="#64748b" font-size="10" text-anchor="middle">tool results → messages[] → compress → load memories → LLM → extract after each turn</text>
<!-- ===== Bottom notes ===== -->
<rect x="40" y="358" width="680" height="56" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
<rect x="60" y="372" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="80" y="382" fill="#475569" font-size="10">s08 preserved: compression pipeline (budget → snip → micro → auto) + emergency trim + loop</text>
<rect x="60" y="392" width="12" height="10" rx="2" fill="#f3e8ff" stroke="#7c3aed" stroke-width="1"/>
<text x="80" y="402" fill="#475569" font-size="10">s09 new: Loading (index in SYSTEM + on-demand inject) + Extraction (after each turn) + Consolidation (threshold)</text>
</svg>

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@@ -0,0 +1,104 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 430" font-family="system-ui, -apple-system, sans-serif">
<defs>
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#7c3aed"/>
</linearGradient>
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
</marker>
<marker id="arrow-purple" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#7c3aed"/>
</marker>
<marker id="arrow-green" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#16a34a"/>
</marker>
</defs>
<rect width="760" height="430" fill="#fafbfc" rx="8"/>
<!-- Title -->
<rect x="0" y="0" width="760" height="44" fill="url(#header)" rx="8"/>
<rect x="0" y="36" width="760" height="8" fill="url(#header)"/>
<text x="380" y="28" fill="#fff" font-size="14" font-weight="700" text-anchor="middle">Memory — s08 圧縮パイプラインに記憶の読み込み・抽出・整理を挿入</text>
<!-- Legend -->
<rect x="40" y="56" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="58" y="66" fill="#2563eb" font-size="10" font-weight="600">s08 維持</text>
<rect x="130" y="56" width="12" height="10" rx="2" fill="#f3e8ff" stroke="#7c3aed" stroke-width="1"/>
<text x="148" y="66" fill="#7c3aed" font-size="10" font-weight="600">s09 追加</text>
<!-- ===== messages[] ===== -->
<rect x="30" y="96" width="100" height="52" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="80" y="126" fill="#1e3a5f" font-size="12" font-weight="600" text-anchor="middle">messages[]</text>
<!-- arrow → compression -->
<line x1="130" y1="122" x2="152" y2="122" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- ===== Compression pipeline (s08) ===== -->
<rect x="155" y="86" width="135" height="72" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="222" y="108" fill="#1e3a5f" font-size="11" font-weight="700" text-anchor="middle">圧縮パイプライン</text>
<text x="222" y="124" fill="#64748b" font-size="9" text-anchor="middle">budget → snip → micro</text>
<text x="222" y="138" fill="#64748b" font-size="9" text-anchor="middle">→ autoCompact</text>
<text x="222" y="152" fill="#94a3b8" font-size="8" text-anchor="middle">(s08)</text>
<!-- arrow → Loading (purple) -->
<line x1="290" y1="122" x2="317" y2="122" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow-purple)"/>
<!-- ===== Loading (s09) ===== -->
<rect x="320" y="86" width="120" height="72" rx="8" fill="#f3e8ff" stroke="#7c3aed" stroke-width="2"/>
<text x="380" y="108" fill="#5b21b6" font-size="11" font-weight="700" text-anchor="middle">Loading</text>
<text x="380" y="124" fill="#7c3aed" font-size="9" text-anchor="middle">LLM side-query 選択</text>
<text x="380" y="138" fill="#7c3aed" font-size="9" text-anchor="middle">ファイル内容を注入</text>
<text x="380" y="152" fill="#a78bfa" font-size="8" text-anchor="middle">≤ 5 件</text>
<!-- arrow → LLM -->
<line x1="440" y1="122" x2="472" y2="122" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- ===== LLM (s08) ===== -->
<rect x="475" y="96" width="80" height="52" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="515" y="114" fill="#1e3a5f" font-size="14" font-weight="700" text-anchor="middle">LLM</text>
<text x="515" y="132" fill="#64748b" font-size="9" text-anchor="middle">stop_reason</text>
<text x="515" y="144" fill="#64748b" font-size="9" text-anchor="middle">=tool_use?</text>
<!-- LLM → no → return result -->
<line x1="515" y1="148" x2="515" y2="178" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
<text x="528" y="168" fill="#16a34a" font-size="9" font-weight="600">なし、停止</text>
<rect x="460" y="180" width="110" height="24" rx="12" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
<text x="515" y="196" fill="#166534" font-size="10" font-weight="600" text-anchor="middle">結果を返す</text>
<!-- LLM → yes → TOOL_HANDLERS -->
<line x1="555" y1="122" x2="587" y2="122" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<text x="568" y="114" fill="#64748b" font-size="9" font-weight="600">あり</text>
<!-- ===== TOOL_HANDLERS (s08) ===== -->
<rect x="590" y="88" width="130" height="68" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="655" y="112" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">TOOL_HANDLERS</text>
<text x="655" y="128" fill="#64748b" font-size="9" text-anchor="middle">bash · read · write</text>
<text x="655" y="142" fill="#94a3b8" font-size="8" text-anchor="middle">edit · glob · task</text>
<!-- ===== Memory Files (s09) ===== -->
<rect x="155" y="232" width="430" height="36" rx="6" fill="#faf5ff" stroke="#7c3aed" stroke-width="1.5" stroke-dasharray="4,2"/>
<text x="370" y="255" fill="#5b21b6" font-size="11" font-weight="600" text-anchor="middle">.memory/ — MEMORY.md インデックス + *.md ファイル(セッション間永続化)</text>
<!-- Arrow: Memory Files → Loading -->
<path d="M 395 232 L 395 162" fill="none" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow-purple)"/>
<text x="408" y="200" fill="#7c3aed" font-size="9">読み込み</text>
<!-- Arrow: return result → Extraction → Memory Files -->
<path d="M 515 204 L 515 232" fill="none" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow-purple)"/>
<text x="528" y="222" fill="#7c3aed" font-size="9">Extraction毎ターン終了後</text>
<!-- Consolidation note -->
<text x="222" y="284" fill="#a78bfa" font-size="9">Consolidation: ファイル ≥ 10 でトリガー、重複排除・統合・剪定</text>
<!-- ===== Loop back ===== -->
<path d="M 720 122 L 748 122 Q 756 122 756 130 L 756 310 Q 756 318 748 318 L 88 318 Q 80 318 80 310 L 80 148" fill="none" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
<text x="400" y="340" fill="#64748b" font-size="10" text-anchor="middle">ツール結果 → messages[] → 圧縮 → 記憶読み込み → LLM → 毎ターン終了後に抽出</text>
<!-- ===== Bottom notes ===== -->
<rect x="40" y="358" width="680" height="56" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
<rect x="60" y="372" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="80" y="382" fill="#475569" font-size="10">s08 維持圧縮パイプラインbudget → snip → micro → auto+ 緊急トリム + ループ</text>
<rect x="60" y="392" width="12" height="10" rx="2" fill="#f3e8ff" stroke="#7c3aed" stroke-width="1"/>
<text x="80" y="402" fill="#475569" font-size="10">s09 追加Loadingインデックス常駐 + オンデマンド注入)+ Extraction毎ターン終了後+ Consolidation閾値トリガー</text>
</svg>

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@@ -0,0 +1,104 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 430" font-family="system-ui, -apple-system, sans-serif">
<defs>
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#7c3aed"/>
</linearGradient>
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
</marker>
<marker id="arrow-purple" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#7c3aed"/>
</marker>
<marker id="arrow-green" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#16a34a"/>
</marker>
</defs>
<rect width="760" height="430" fill="#fafbfc" rx="8"/>
<!-- Title -->
<rect x="0" y="0" width="760" height="44" fill="url(#header)" rx="8"/>
<rect x="0" y="36" width="760" height="8" fill="url(#header)"/>
<text x="380" y="28" fill="#fff" font-size="15" font-weight="700" text-anchor="middle">Memory — 在 s08 压缩管线上,插入记忆加载、提取与整理</text>
<!-- Legend -->
<rect x="40" y="56" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="58" y="66" fill="#2563eb" font-size="10" font-weight="600">s08 保留</text>
<rect x="140" y="56" width="12" height="10" rx="2" fill="#f3e8ff" stroke="#7c3aed" stroke-width="1"/>
<text x="158" y="66" fill="#7c3aed" font-size="10" font-weight="600">s09 新增</text>
<!-- ===== messages[] ===== -->
<rect x="30" y="96" width="100" height="52" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="80" y="126" fill="#1e3a5f" font-size="12" font-weight="600" text-anchor="middle">messages[]</text>
<!-- arrow → compression -->
<line x1="130" y1="122" x2="152" y2="122" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- ===== Compression pipeline (s08) ===== -->
<rect x="155" y="86" width="135" height="72" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="222" y="108" fill="#1e3a5f" font-size="11" font-weight="700" text-anchor="middle">压缩管线</text>
<text x="222" y="124" fill="#64748b" font-size="9" text-anchor="middle">budget → snip → micro</text>
<text x="222" y="138" fill="#64748b" font-size="9" text-anchor="middle">→ autoCompact</text>
<text x="222" y="152" fill="#94a3b8" font-size="8" text-anchor="middle">(s08)</text>
<!-- arrow → Loading (purple) -->
<line x1="290" y1="122" x2="317" y2="122" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow-purple)"/>
<!-- ===== Loading (s09) ===== -->
<rect x="320" y="86" width="120" height="72" rx="8" fill="#f3e8ff" stroke="#7c3aed" stroke-width="2"/>
<text x="380" y="108" fill="#5b21b6" font-size="11" font-weight="700" text-anchor="middle">Loading</text>
<text x="380" y="124" fill="#7c3aed" font-size="9" text-anchor="middle">LLM side-query 选文件</text>
<text x="380" y="138" fill="#7c3aed" font-size="9" text-anchor="middle">注入文件内容</text>
<text x="380" y="152" fill="#a78bfa" font-size="8" text-anchor="middle">≤ 5 条</text>
<!-- arrow → LLM -->
<line x1="440" y1="122" x2="472" y2="122" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- ===== LLM (s08) ===== -->
<rect x="475" y="96" width="80" height="52" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="515" y="114" fill="#1e3a5f" font-size="14" font-weight="700" text-anchor="middle">LLM</text>
<text x="515" y="132" fill="#64748b" font-size="9" text-anchor="middle">stop_reason</text>
<text x="515" y="144" fill="#64748b" font-size="9" text-anchor="middle">=tool_use?</text>
<!-- LLM → 否 → 返回结果 -->
<line x1="515" y1="148" x2="515" y2="178" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
<text x="528" y="168" fill="#16a34a" font-size="9" font-weight="600">否,停止</text>
<rect x="460" y="180" width="110" height="24" rx="12" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
<text x="515" y="196" fill="#166534" font-size="10" font-weight="600" text-anchor="middle">返回结果</text>
<!-- LLM → 是 → TOOL_HANDLERS -->
<line x1="555" y1="122" x2="587" y2="122" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
<text x="568" y="114" fill="#64748b" font-size="9" font-weight="600"></text>
<!-- ===== TOOL_HANDLERS (s08) ===== -->
<rect x="590" y="88" width="130" height="68" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
<text x="655" y="112" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">TOOL_HANDLERS</text>
<text x="655" y="128" fill="#64748b" font-size="9" text-anchor="middle">bash · read · write</text>
<text x="655" y="142" fill="#94a3b8" font-size="8" text-anchor="middle">edit · glob · task</text>
<!-- ===== Memory Files (s09) ===== -->
<rect x="155" y="232" width="430" height="36" rx="6" fill="#faf5ff" stroke="#7c3aed" stroke-width="1.5" stroke-dasharray="4,2"/>
<text x="370" y="255" fill="#5b21b6" font-size="11" font-weight="600" text-anchor="middle">.memory/ — MEMORY.md 索引 + *.md 文件(跨会话持久化)</text>
<!-- Arrow: Memory Files → Loading -->
<path d="M 395 232 L 395 162" fill="none" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow-purple)"/>
<text x="408" y="200" fill="#7c3aed" font-size="9">读取</text>
<!-- Arrow: 返回结果 → Extraction → Memory Files -->
<path d="M 515 204 L 515 232" fill="none" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow-purple)"/>
<text x="528" y="222" fill="#7c3aed" font-size="9">Extraction每轮结束后</text>
<!-- Consolidation note -->
<text x="222" y="284" fill="#a78bfa" font-size="9">Consolidation: 文件数 ≥ 10 时触发,去重·合并·剪枝</text>
<!-- ===== Loop back ===== -->
<path d="M 720 122 L 748 122 Q 756 122 756 130 L 756 310 Q 756 318 748 318 L 88 318 Q 80 318 80 310 L 80 148" fill="none" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
<text x="400" y="340" fill="#64748b" font-size="10" text-anchor="middle">工具结果追加到 messages[] → 压缩 → 加载记忆 → LLM → 每轮结束后提取</text>
<!-- ===== Bottom notes ===== -->
<rect x="40" y="358" width="680" height="56" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
<rect x="60" y="372" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
<text x="80" y="382" fill="#475569" font-size="10">s08 保留压缩管线budget → snip → micro → auto+ 应急裁剪 + 循环</text>
<rect x="60" y="392" width="12" height="10" rx="2" fill="#f3e8ff" stroke="#7c3aed" stroke-width="1"/>
<text x="80" y="402" fill="#475569" font-size="10">s09 新增Loading索引常驻 + 按需注入)+ Extraction每轮结束后+ Consolidation阈值触发</text>
</svg>

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@@ -0,0 +1,78 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 380" font-family="system-ui, -apple-system, sans-serif">
<defs>
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#7c3aed"/>
</linearGradient>
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#7c3aed"/>
</marker>
</defs>
<rect width="720" height="380" fill="#fafbfc" rx="8"/>
<rect x="0" y="0" width="720" height="38" fill="url(#header)" rx="8"/>
<rect x="0" y="30" width="720" height="8" fill="url(#header)"/>
<text x="360" y="25" fill="#fff" font-size="14" font-weight="700" text-anchor="middle">Memory System — Store · Load · Extract · Consolidate</text>
<!-- Storage -->
<rect x="40" y="58" width="145" height="80" rx="8" fill="#ede9fe" stroke="#7c3aed" stroke-width="2"/>
<text x="112" y="80" fill="#5b21b6" font-size="13" font-weight="700" text-anchor="middle">Storage</text>
<line x1="55" y1="90" x2="170" y2="90" stroke="#c4b5fd" stroke-width="0.5"/>
<text x="55" y="108" fill="#5b21b6" font-size="10">.memory/*.md files</text>
<text x="55" y="124" fill="#5b21b6" font-size="10">MEMORY.md index</text>
<line x1="190" y1="98" x2="218" y2="98" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- Loading -->
<rect x="222" y="58" width="200" height="80" rx="8" fill="#ede9fe" stroke="#7c3aed" stroke-width="2"/>
<text x="322" y="80" fill="#5b21b6" font-size="13" font-weight="700" text-anchor="middle">Load</text>
<line x1="237" y1="90" x2="407" y2="90" stroke="#c4b5fd" stroke-width="0.5"/>
<text x="237" y="108" fill="#5b21b6" font-size="10">Index in SYSTEM (always)</text>
<text x="237" y="124" fill="#5b21b6" font-size="10">LLM side-query select files</text>
<text x="237" y="134" fill="#a78bfa" font-size="9">≤ 5 items, fallback to keyword</text>
<line x1="425" y1="98" x2="453" y2="98" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- Extraction -->
<rect x="457" y="58" width="130" height="80" rx="8" fill="#f3e8ff" stroke="#7c3aed" stroke-width="2"/>
<text x="522" y="80" fill="#5b21b6" font-size="13" font-weight="700" text-anchor="middle">Extract</text>
<line x1="472" y1="90" x2="572" y2="90" stroke="#c4b5fd" stroke-width="0.5"/>
<text x="472" y="108" fill="#5b21b6" font-size="10">After each turn ends</text>
<text x="472" y="124" fill="#5b21b6" font-size="10">LLM extracts prefs/constraints</text>
<text x="472" y="134" fill="#a78bfa" font-size="9">Check existing, avoid duplicates</text>
<!-- Consolidation -->
<rect x="600" y="58" width="100" height="80" rx="8" fill="#f5f3ff" stroke="#7c3aed" stroke-width="2"/>
<text x="650" y="80" fill="#5b21b6" font-size="13" font-weight="700" text-anchor="middle">Consolidate</text>
<line x1="615" y1="90" x2="685" y2="90" stroke="#c4b5fd" stroke-width="0.5"/>
<text x="615" y="108" fill="#5b21b6" font-size="10">Triggers at ≥ 10 files</text>
<text x="615" y="124" fill="#5b21b6" font-size="10">Dedup · merge · prune</text>
<text x="615" y="134" fill="#a78bfa" font-size="9">CC: 3-layer gating</text>
<!-- Memory Files -->
<rect x="40" y="180" width="660" height="36" rx="6" fill="#f8fafc" stroke="#94a3b8" stroke-width="1" stroke-dasharray="4,2"/>
<text x="370" y="203" fill="#475569" font-size="11" text-anchor="middle">.memory/ — MEMORY.md index + *.md files (YAML frontmatter: name / description / type)</text>
<!-- Arrow: Storage → Memory Files -->
<path d="M 112 138 L 112 174 Q 112 180 118 180" fill="none" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow)"/>
<text x="120" y="162" fill="#7c3aed" font-size="9">read/write</text>
<!-- Arrow: Extraction → Memory Files -->
<path d="M 522 138 L 522 180" fill="none" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow)"/>
<text x="536" y="164" fill="#7c3aed" font-size="9">write</text>
<!-- Arrow: Consolidation → Memory Files -->
<path d="M 650 138 L 650 180" fill="none" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow)"/>
<text x="662" y="164" fill="#7c3aed" font-size="9">overwrite</text>
<!-- Four types -->
<rect x="40" y="240" width="660" height="40" rx="6" fill="#faf5ff" stroke="#c4b5fd" stroke-width="0.5"/>
<text x="60" y="260" fill="#5b21b6" font-size="10" font-weight="600">Four types:</text>
<text x="140" y="260" fill="#475569" font-size="10">user (who you are) · feedback (how to work) · project (what's happening) · reference (where to find things)</text>
<!-- CC source comparison -->
<rect x="40" y="296" width="660" height="72" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
<text x="60" y="316" fill="#5b21b6" font-size="11" font-weight="600">CC Source Comparison</text>
<text x="60" y="334" fill="#475569" font-size="10">• Selection: LLM side-query (Sonnet selects), not embedding vector similarity</text>
<text x="60" y="350" fill="#475569" font-size="10">• Extraction timing: stop hook (after each turn ends), not after autoCompact</text>
<text x="60" y="366" fill="#475569" font-size="10">• Dream consolidation: 3-layer gating (time ≥ 24h + sessions ≥ 5 + file lock), not simple count</text>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -0,0 +1,78 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 380" font-family="system-ui, -apple-system, sans-serif">
<defs>
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#7c3aed"/>
</linearGradient>
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#7c3aed"/>
</marker>
</defs>
<rect width="720" height="380" fill="#fafbfc" rx="8"/>
<rect x="0" y="0" width="720" height="38" fill="url(#header)" rx="8"/>
<rect x="0" y="30" width="720" height="8" fill="url(#header)"/>
<text x="360" y="25" fill="#fff" font-size="14" font-weight="700" text-anchor="middle">Memory System — ストレージ · 読み込み · 抽出 · 整理</text>
<!-- ストレージ -->
<rect x="40" y="58" width="145" height="80" rx="8" fill="#ede9fe" stroke="#7c3aed" stroke-width="2"/>
<text x="112" y="80" fill="#5b21b6" font-size="13" font-weight="700" text-anchor="middle">ストレージ</text>
<line x1="55" y1="90" x2="170" y2="90" stroke="#c4b5fd" stroke-width="0.5"/>
<text x="55" y="108" fill="#5b21b6" font-size="10">.memory/*.md ファイル</text>
<text x="55" y="124" fill="#5b21b6" font-size="10">MEMORY.md インデックス</text>
<line x1="190" y1="98" x2="218" y2="98" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- 読み込み -->
<rect x="222" y="58" width="200" height="80" rx="8" fill="#ede9fe" stroke="#7c3aed" stroke-width="2"/>
<text x="322" y="80" fill="#5b21b6" font-size="13" font-weight="700" text-anchor="middle">読み込み</text>
<line x1="237" y1="90" x2="407" y2="90" stroke="#c4b5fd" stroke-width="0.5"/>
<text x="237" y="108" fill="#5b21b6" font-size="10">インデックスを SYSTEM に常駐</text>
<text x="237" y="124" fill="#5b21b6" font-size="10">LLM side-query でファイル選択</text>
<text x="237" y="134" fill="#a78bfa" font-size="9">≤ 5 件、失敗時はキーワードに降格</text>
<line x1="425" y1="98" x2="453" y2="98" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- 抽出 -->
<rect x="457" y="58" width="130" height="80" rx="8" fill="#f3e8ff" stroke="#7c3aed" stroke-width="2"/>
<text x="522" y="80" fill="#5b21b6" font-size="13" font-weight="700" text-anchor="middle">抽出</text>
<line x1="472" y1="90" x2="572" y2="90" stroke="#c4b5fd" stroke-width="0.5"/>
<text x="472" y="108" fill="#5b21b6" font-size="10">毎ターン終了後にトリガー</text>
<text x="472" y="124" fill="#5b21b6" font-size="10">LLM が好み/制約を抽出</text>
<text x="472" y="134" fill="#a78bfa" font-size="9">既存を確認、重複回避</text>
<!-- 整理 -->
<rect x="600" y="58" width="100" height="80" rx="8" fill="#f5f3ff" stroke="#7c3aed" stroke-width="2"/>
<text x="650" y="80" fill="#5b21b6" font-size="13" font-weight="700" text-anchor="middle">整理</text>
<line x1="615" y1="90" x2="685" y2="90" stroke="#c4b5fd" stroke-width="0.5"/>
<text x="615" y="108" fill="#5b21b6" font-size="10">ファイル ≥ 10 でトリガー</text>
<text x="615" y="124" fill="#5b21b6" font-size="10">重複排除・統合・剪定</text>
<text x="615" y="134" fill="#a78bfa" font-size="9">CC: 3 層ゲート</text>
<!-- Memory Files -->
<rect x="40" y="180" width="660" height="36" rx="6" fill="#f8fafc" stroke="#94a3b8" stroke-width="1" stroke-dasharray="4,2"/>
<text x="370" y="203" fill="#475569" font-size="11" text-anchor="middle">.memory/ — MEMORY.md インデックス + *.md ファイルYAML frontmatter: name / description / type</text>
<!-- Arrow: ストレージ → Memory Files -->
<path d="M 112 138 L 112 174 Q 112 180 118 180" fill="none" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow)"/>
<text x="120" y="162" fill="#7c3aed" font-size="9">読み/書き</text>
<!-- Arrow: 抽出 → Memory Files -->
<path d="M 522 138 L 522 180" fill="none" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow)"/>
<text x="536" y="164" fill="#7c3aed" font-size="9">書き込み</text>
<!-- Arrow: 整理 → Memory Files -->
<path d="M 650 138 L 650 180" fill="none" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow)"/>
<text x="662" y="164" fill="#7c3aed" font-size="9">上書き</text>
<!-- 4 種類の記憶 -->
<rect x="40" y="240" width="660" height="40" rx="6" fill="#faf5ff" stroke="#c4b5fd" stroke-width="0.5"/>
<text x="60" y="260" fill="#5b21b6" font-size="10" font-weight="600">4 種類の記憶:</text>
<text x="148" y="260" fill="#475569" font-size="10">userあなたは誰か· feedbackどう作業するか· project何が起きているか· referenceどこで探すか</text>
<!-- CC ソースコード対照 -->
<rect x="40" y="296" width="660" height="72" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
<text x="60" y="316" fill="#5b21b6" font-size="11" font-weight="600">CC ソースコード対照</text>
<text x="60" y="334" fill="#475569" font-size="10">• 記憶選択LLM side-querySonnet が選択、embedding ベクトル類似度ではない</text>
<text x="60" y="350" fill="#475569" font-size="10">• 抽出タイミングstop hook毎ターン終了後、autoCompact 後ではない</text>
<text x="60" y="366" fill="#475569" font-size="10">• Dream 整理3 層ゲート(時間 ≥ 24h + セッション ≥ 5 + ファイルロック)、単純な計数ではない</text>
</svg>

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@@ -0,0 +1,78 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 380" font-family="system-ui, -apple-system, sans-serif">
<defs>
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#7c3aed"/>
</linearGradient>
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#7c3aed"/>
</marker>
</defs>
<rect width="720" height="380" fill="#fafbfc" rx="8"/>
<rect x="0" y="0" width="720" height="38" fill="url(#header)" rx="8"/>
<rect x="0" y="30" width="720" height="8" fill="url(#header)"/>
<text x="360" y="25" fill="#fff" font-size="14" font-weight="700" text-anchor="middle">Memory System — 存储 · 加载 · 提取 · 整理</text>
<!-- 存储 -->
<rect x="40" y="58" width="145" height="80" rx="8" fill="#ede9fe" stroke="#7c3aed" stroke-width="2"/>
<text x="112" y="80" fill="#5b21b6" font-size="13" font-weight="700" text-anchor="middle">存储</text>
<line x1="55" y1="90" x2="170" y2="90" stroke="#c4b5fd" stroke-width="0.5"/>
<text x="55" y="108" fill="#5b21b6" font-size="10">.memory/*.md 文件</text>
<text x="55" y="124" fill="#5b21b6" font-size="10">MEMORY.md 索引</text>
<line x1="190" y1="98" x2="218" y2="98" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- 加载 -->
<rect x="222" y="58" width="200" height="80" rx="8" fill="#ede9fe" stroke="#7c3aed" stroke-width="2"/>
<text x="322" y="80" fill="#5b21b6" font-size="13" font-weight="700" text-anchor="middle">加载</text>
<line x1="237" y1="90" x2="407" y2="90" stroke="#c4b5fd" stroke-width="0.5"/>
<text x="237" y="108" fill="#5b21b6" font-size="10">索引常驻 SYSTEM</text>
<text x="237" y="124" fill="#5b21b6" font-size="10">LLM side-query 选文件</text>
<text x="237" y="134" fill="#a78bfa" font-size="9">≤ 5 条,失败降级到关键词</text>
<line x1="425" y1="98" x2="453" y2="98" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- 提取 -->
<rect x="457" y="58" width="130" height="80" rx="8" fill="#f3e8ff" stroke="#7c3aed" stroke-width="2"/>
<text x="522" y="80" fill="#5b21b6" font-size="13" font-weight="700" text-anchor="middle">提取</text>
<line x1="472" y1="90" x2="572" y2="90" stroke="#c4b5fd" stroke-width="0.5"/>
<text x="472" y="108" fill="#5b21b6" font-size="10">每轮结束后触发</text>
<text x="472" y="124" fill="#5b21b6" font-size="10">LLM 提取偏好/约束</text>
<text x="472" y="134" fill="#a78bfa" font-size="9">检查已有,避免重复</text>
<!-- 整理 -->
<rect x="600" y="58" width="100" height="80" rx="8" fill="#f5f3ff" stroke="#7c3aed" stroke-width="2"/>
<text x="650" y="80" fill="#5b21b6" font-size="13" font-weight="700" text-anchor="middle">整理</text>
<line x1="615" y1="90" x2="685" y2="90" stroke="#c4b5fd" stroke-width="0.5"/>
<text x="615" y="108" fill="#5b21b6" font-size="10">文件 ≥ 10 触发</text>
<text x="615" y="124" fill="#5b21b6" font-size="10">去重·合并·剪枝</text>
<text x="615" y="134" fill="#a78bfa" font-size="9">CC: 三层门控</text>
<!-- Memory Files -->
<rect x="40" y="180" width="660" height="36" rx="6" fill="#f8fafc" stroke="#94a3b8" stroke-width="1" stroke-dasharray="4,2"/>
<text x="370" y="203" fill="#475569" font-size="11" text-anchor="middle">.memory/ — MEMORY.md 索引 + *.md 文件YAML frontmatter: name / description / type</text>
<!-- Arrow: 存储 → Memory Files -->
<path d="M 112 138 L 112 174 Q 112 180 118 180" fill="none" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow)"/>
<text x="120" y="162" fill="#7c3aed" font-size="9">写入/读取</text>
<!-- Arrow: Extraction → Memory Files -->
<path d="M 522 138 L 522 180" fill="none" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow)"/>
<text x="536" y="164" fill="#7c3aed" font-size="9">写入</text>
<!-- Arrow: 整理 → Memory Files -->
<path d="M 650 138 L 650 180" fill="none" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow)"/>
<text x="662" y="164" fill="#7c3aed" font-size="9">覆写</text>
<!-- 四类记忆 -->
<rect x="40" y="240" width="660" height="40" rx="6" fill="#faf5ff" stroke="#c4b5fd" stroke-width="0.5"/>
<text x="60" y="260" fill="#5b21b6" font-size="10" font-weight="600">四类记忆:</text>
<text x="140" y="260" fill="#475569" font-size="10">user你是谁· feedback怎么做事· project正在发生什么· reference东西在哪找</text>
<!-- CC 源码对照 -->
<rect x="40" y="296" width="660" height="72" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
<text x="60" y="316" fill="#5b21b6" font-size="11" font-weight="600">CC 源码对照</text>
<text x="60" y="334" fill="#475569" font-size="10">• 记忆选择LLM side-querySonnet 选),不是 embedding 向量相似度</text>
<text x="60" y="350" fill="#475569" font-size="10">• 提取时机stop hook 中触发(每轮结束后),不是 autoCompact 后</text>
<text x="60" y="366" fill="#475569" font-size="10">• Dream 整理:三层门控(时间 ≥ 24h + 会话 ≥ 5 + 文件锁),不是简单计数</text>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB