mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
- Set up VitePress with bilingual support (Chinese root + English /en/) - Create unified sidebar navigation with all doc sections - Add custom Anthropic brand theme (#D97757) - Create Quick Start pages (zh/en) from README content - Migrate 7 .en.md files to docs/en/ directory structure - Translate memory/agent/skills docs to English (9 files) - Generate English-only architecture diagrams (11 images via PicTactic) - Copy English-ready images for agent/skills sections (21 images) - Configure custom domain (claudecodehaha.relakkesyang.org) - Localize Chinese UI labels (sidebar, outline, footer) - Add documentation site badge to READMEs - Fix dead links and update cross-references for VitePress Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
481 lines
18 KiB
Markdown
481 lines
18 KiB
Markdown
# Claude Code Memory System — Implementation Details
|
|
|
|
> From system prompt injection to background auto-extraction, dissecting every technical detail of the memory system.
|
|
|
|
<p align="center">
|
|
<a href="#1-overall-architecture">Architecture</a> · <a href="#2-path-resolution-system">Path Resolution</a> · <a href="#3-system-prompt-injection">Prompt Injection</a> · <a href="#4-automatic-memory-extraction">Auto-Extraction</a> · <a href="#5-intelligent-memory-retrieval">Intelligent Retrieval</a> · <a href="#6-memory-scanning-in-detail">Memory Scanning</a> · <a href="#7-agent-memory">Agent Memory</a> · <a href="#8-team-memory-sync">Team Sync</a> · <a href="#9-key-constants-quick-reference">Constants</a> · <a href="#10-data-flow-overview">Data Flow</a>
|
|
</p>
|
|
|
|

|
|
|
|
---
|
|
|
|
## 1. Overall Architecture
|
|
|
|
The memory system is powered by 5 core modules working in concert:
|
|
|
|
| Module | Source Location | Responsibility |
|
|
|--------|----------------|----------------|
|
|
| **Path Resolution** | `src/memdir/paths.ts` | Computes memory storage directory, handles overrides and security validation |
|
|
| **Prompt Construction** | `src/memdir/memdir.ts` | Injects memory instructions into the system prompt |
|
|
| **Memory Scanning** | `src/memdir/memoryScan.ts` | Scans directories, parses frontmatter, sorts entries |
|
|
| **Intelligent Retrieval** | `src/memdir/findRelevantMemories.ts` | Uses Sonnet to select memories relevant to the current query |
|
|
| **Auto-Extraction** | `src/services/extractMemories/` | Background forked agent that extracts memories from conversations |
|
|
|
|
Auxiliary modules:
|
|
|
|
| Module | Source Location | Responsibility |
|
|
|--------|----------------|----------------|
|
|
| **AutoDream** | `src/services/autoDream/` | Background memory consolidation ("dreaming"); see [03-autodream.md](./03-autodream.md) |
|
|
| **Type Definitions** | `src/memdir/memoryTypes.ts` | Taxonomy and prompt templates for the four memory types |
|
|
| **Freshness** | `src/memdir/memoryAge.ts` | Calculates memory age, generates stale warnings |
|
|
| **File Detection** | `src/utils/memoryFileDetection.ts` | Determines whether a path belongs to the memory system |
|
|
| **Agent Memory** | `src/tools/AgentTool/agentMemory.ts` | Three-level memory directories exclusive to sub-agents |
|
|
| **Team Sync** | `src/services/teamMemorySync/` | Remote upload/download of memories |
|
|
|
|
---
|
|
|
|
## 2. Path Resolution System
|
|
|
|

|
|
|
|
### Core Function: `getAutoMemPath()`
|
|
|
|
```
|
|
Path resolution priority (highest to lowest):
|
|
|
|
1. CLAUDE_COWORK_MEMORY_PATH_OVERRIDE <- Cowork environment variable (full path)
|
|
2. settings.json -> autoMemoryDirectory <- User setting (supports ~/ expansion)
|
|
3. {memoryBase}/projects/{sanitized-git-root}/memory/ <- Default computed path
|
|
```
|
|
|
|
**Key source** `src/memdir/paths.ts:223`:
|
|
|
|
```typescript
|
|
export const getAutoMemPath = memoize(
|
|
(): string => {
|
|
const override = getAutoMemPathOverride() ?? getAutoMemPathSetting()
|
|
if (override) return override
|
|
const projectsDir = join(getMemoryBaseDir(), 'projects')
|
|
return join(projectsDir, sanitizePath(getAutoMemBase()), AUTO_MEM_DIRNAME) + sep
|
|
},
|
|
() => getProjectRoot(), // Cache key = project root
|
|
)
|
|
```
|
|
|
|
### Path Security Validation
|
|
|
|
`validateMemoryPath()` rejects dangerous paths:
|
|
|
|
| Rejected Path | Reason |
|
|
|---------------|--------|
|
|
| `../foo` | Relative path, CWD-dependent |
|
|
| `/` or `/a` | Root path or too-short path |
|
|
| `C:\` | Windows drive root |
|
|
| `\\server\share` | UNC network path |
|
|
| Contains `\0` | Null byte, can truncate in system calls |
|
|
|
|
**Security restriction**: Project-level `.claude/settings.json` is **not allowed** to set `autoMemoryDirectory`, preventing malicious repositories from gaining write access to sensitive directories like `~/.ssh`.
|
|
|
|
### Enable Conditions
|
|
|
|
The `isAutoMemoryEnabled()` check chain:
|
|
|
|
```
|
|
CLAUDE_CODE_DISABLE_AUTO_MEMORY=1 -> Disabled
|
|
CLAUDE_CODE_SIMPLE (--bare) -> Disabled
|
|
Remote mode without REMOTE_MEMORY_DIR -> Disabled
|
|
settings.json autoMemoryEnabled -> Follows setting
|
|
Default -> Enabled
|
|
```
|
|
|
|
---
|
|
|
|
## 3. System Prompt Injection
|
|
|
|

|
|
|
|
### Entry Point: `loadMemoryPrompt()`
|
|
|
|
This is the interface between the memory system and the system prompt. It is called once at startup (cached via `systemPromptSection`).
|
|
|
|
```
|
|
loadMemoryPrompt()
|
|
|-- KAIROS mode? -> buildAssistantDailyLogPrompt() [log append mode]
|
|
|-- TEAMMEM enabled? -> buildCombinedMemoryPrompt() [personal + team dual directory]
|
|
|-- Normal mode -> buildMemoryLines() [single directory]
|
|
|-- Disabled -> return null
|
|
```
|
|
|
|
### Prompt Structure Built by `buildMemoryLines()`
|
|
|
|
```
|
|
# auto memory
|
|
|
|
You have a persistent file-based memory system located at `{memoryDir}`...
|
|
|
|
## Types of memory <- Definitions and examples for all four types
|
|
## What NOT to save <- Exclusion rules
|
|
## How to save memories <- Two-step saving process
|
|
## When to access memories <- When to consult
|
|
## Before recommending <- Verify before citing
|
|
## Memory and other forms <- Distinction from Plan/Task
|
|
|
|
## MEMORY.md <- Index content (or "currently empty")
|
|
```
|
|
|
|
### MEMORY.md Truncation Strategy
|
|
|
|
`truncateEntrypointContent()` applies dual limits:
|
|
|
|
```typescript
|
|
// First truncate by line count
|
|
if (lineCount > 200) -> Truncate to 200 lines
|
|
|
|
// Then truncate by byte size (handles extra-long lines)
|
|
if (bytes > 25,000) -> Truncate at last newline character
|
|
|
|
// Append warning
|
|
"WARNING: MEMORY.md is {reason}. Only part of it was loaded."
|
|
```
|
|
|
|
### Automatic Directory Creation
|
|
|
|
`ensureMemoryDirExists()` ensures the directory exists when loading the prompt:
|
|
- Recursively creates with `mkdir` (handles the entire parent chain)
|
|
- Swallows `EEXIST` (idempotent)
|
|
- Genuine permission errors are only logged, not thrown (the Write tool will surface the real error)
|
|
|
|
The prompt explicitly tells the model the directory already exists, avoiding wasted turns on `ls` or `mkdir`.
|
|
|
|
---
|
|
|
|
## 4. Automatic Memory Extraction
|
|
|
|

|
|
|
|
### Trigger Timing
|
|
|
|
Triggered in `handleStopHooks` when the model produces a final response (no tool calls).
|
|
|
|
**Key source**: `src/services/extractMemories/extractMemories.ts`
|
|
|
|
### Complete Extraction Flow
|
|
|
|
```
|
|
1. Model finishes response (no tool_use)
|
|
|
|
|
2. executeExtractMemories() is called
|
|
|
|
|
3. Guard checks:
|
|
- Is this the main agent? (sub-agents don't extract)
|
|
- Feature gate enabled?
|
|
- Auto-memory enabled?
|
|
- Not in remote mode?
|
|
- No parallel extraction in progress?
|
|
|
|
|
4. Frequency control:
|
|
turnsSinceLastExtraction++
|
|
if < tengu_bramble_lintel -> skip
|
|
|
|
|
5. Mutual exclusion check:
|
|
Main agent wrote memory itself? -> skip, advance cursor
|
|
|
|
|
6. Scan existing memory directory (scanMemoryFiles)
|
|
Generate manifest (formatMemoryManifest)
|
|
|
|
|
7. Build extraction prompt (buildExtractAutoOnlyPrompt)
|
|
|
|
|
8. Run forked agent (runForkedAgent)
|
|
- Shares parent session's prompt cache
|
|
- Max 5 turns
|
|
- Restricted tool permissions
|
|
|
|
|
9. Extract written file paths
|
|
Advance cursor to latest message
|
|
|
|
|
10. Notify user: "Memory updated in ..."
|
|
```
|
|
|
|
### Forked Agent
|
|
|
|
Auto-extraction uses `runForkedAgent` -- a perfect fork of the main session:
|
|
|
|
- **Shared prompt cache**: Avoids duplicate API cache creation costs
|
|
- **Isolated execution**: Does not affect the main session's message history
|
|
- **Restricted tools**: Only allows Read, Grep, Glob, read-only Bash, and Edit/Write within the memory directory
|
|
- **No transcript recording**: Prevents race conditions with the main thread
|
|
|
|
### Tool Permissions (`createAutoMemCanUseTool`)
|
|
|
|
```
|
|
Allowed: Read, Grep, Glob (unrestricted)
|
|
Allowed: Bash (read-only commands only: ls, find, grep, cat, stat...)
|
|
Allowed: Edit/Write (only within auto-memory directory)
|
|
Denied: MCP, Agent, non-read-only Bash, other write operations
|
|
```
|
|
|
|
### Mutual Exclusion Mechanism
|
|
|
|
The main agent and the extraction agent are **mutually exclusive**:
|
|
|
|
```typescript
|
|
function hasMemoryWritesSince(messages, sinceUuid): boolean {
|
|
// Scan all assistant messages after sinceUuid
|
|
// If any Edit/Write tool_use targets the auto-memory directory
|
|
// -> return true (skip extraction, advance cursor)
|
|
}
|
|
```
|
|
|
|
This prevents duplicate saves: when the main agent has already written a memory, the background extraction is skipped.
|
|
|
|
### Merge Mechanism
|
|
|
|
If a previous extraction is still running:
|
|
1. The new context is queued (`pendingContext`)
|
|
2. After the old extraction completes, a **tail extraction** is immediately launched
|
|
3. The tail extraction only processes messages added between the two calls
|
|
|
|
---
|
|
|
|
## 5. Intelligent Memory Retrieval
|
|
|
|

|
|
|
|
### How It Works
|
|
|
|
Each time the user sends a query, `findRelevantMemories()` is triggered:
|
|
|
|
```
|
|
1. scanMemoryFiles(memoryDir)
|
|
- Recursively reads all .md files (excludes MEMORY.md)
|
|
- Parses frontmatter (first 30 lines)
|
|
- Sorts by modification time in descending order
|
|
- Max 200 files
|
|
|
|
|
2. Filter out previously surfaced memories (alreadySurfaced)
|
|
|
|
|
3. Format manifest (formatMemoryManifest)
|
|
- [type] filename (ISO timestamp): description
|
|
|
|
|
4. Sonnet model selection (sideQuery)
|
|
- System prompt: You are a memory selector...
|
|
- User message: Query + Available memories + Recently used tools
|
|
- Output: JSON { selected_memories: string[] }
|
|
- Max 5 selections
|
|
|
|
|
5. Return selected memories as { path, mtimeMs }
|
|
```
|
|
|
|
### Sonnet Selector Prompt
|
|
|
|
```
|
|
You are selecting memories useful for Claude Code to handle the user's query.
|
|
You'll receive the user's query and a list of available memory files (with filenames and descriptions).
|
|
|
|
Return at most 5 memory filenames that are clearly useful.
|
|
- If uncertain whether something is useful, don't select it
|
|
- If nothing is clearly useful, return an empty list
|
|
- If a list of recently used tools is provided, don't select usage docs for those tools
|
|
(but DO select warnings/gotchas/known issues about those tools)
|
|
```
|
|
|
|
### Freshness Warning
|
|
|
|
Selected memories are injected into the context with freshness information:
|
|
|
|
```typescript
|
|
function memoryFreshnessText(mtimeMs: number): string {
|
|
const d = memoryAgeDays(mtimeMs)
|
|
if (d <= 1) return '' // Today/yesterday: no warning
|
|
return `This memory is ${d} days old. Memories are point-in-time observations...
|
|
Verify against current code before asserting as fact.`
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Memory Scanning in Detail
|
|
|
|
### `scanMemoryFiles()`
|
|
|
|
**Key design**: Single-pass (read-then-sort) to avoid double stat system calls.
|
|
|
|
```typescript
|
|
async function scanMemoryFiles(memoryDir, signal): Promise<MemoryHeader[]> {
|
|
const entries = await readdir(memoryDir, { recursive: true })
|
|
const mdFiles = entries.filter(f => f.endsWith('.md') && basename(f) !== 'MEMORY.md')
|
|
|
|
// Read all files' frontmatter in parallel (first 30 lines)
|
|
const headerResults = await Promise.allSettled(
|
|
mdFiles.map(async (relativePath) => {
|
|
const { content, mtimeMs } = await readFileInRange(filePath, 0, 30)
|
|
const { frontmatter } = parseFrontmatter(content)
|
|
return { filename, filePath, mtimeMs, description, type }
|
|
})
|
|
)
|
|
|
|
// Filter successful results, sort by time descending, take first 200
|
|
return fulfilled.sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, 200)
|
|
}
|
|
```
|
|
|
|
### `formatMemoryManifest()`
|
|
|
|
Generates a manifest consumed by Sonnet or the extraction agent:
|
|
|
|
```
|
|
- [feedback] testing_policy.md (2026-03-15T10:30:00.000Z): Integration tests use real DB
|
|
- [user] role.md (2026-03-14T08:00:00.000Z): Data scientist, focused on logging
|
|
- [project] freeze.md (2026-03-10T15:00:00.000Z): Merge freeze starting 3/5
|
|
```
|
|
|
|
---
|
|
|
|
## 7. Agent Memory
|
|
|
|

|
|
|
|
Sub-agents (launched via the Agent tool) have an independent three-level memory system:
|
|
|
|
| Scope | Path | Description |
|
|
|-------|------|-------------|
|
|
| **user** | `~/.claude/agent-memory/{agentType}/` | Global user-level |
|
|
| **project** | `.claude/agent-memory/{agentType}/` | Project-level (committed to VCS) |
|
|
| **local** | `.claude/agent-memory-local/{agentType}/` | Local-level (not committed) |
|
|
|
|
Differences from main memory:
|
|
- No MEMORY.md index step (`skipIndex = true`)
|
|
- Files can be written directly without the two-step process
|
|
- Each agent type is isolated (explorer, planner, etc. each have their own directory)
|
|
|
|
---
|
|
|
|
## 8. Team Memory Sync
|
|
|
|
When the `TEAMMEM` feature flag is enabled:
|
|
|
|
### Directory Structure
|
|
|
|
```
|
|
~/.claude/projects/{hash}/memory/
|
|
├── MEMORY.md <- Personal memory index
|
|
├── user_*.md <- Personal memories
|
|
└── team/ <- Team shared directory
|
|
├── MEMORY.md <- Team memory index
|
|
└── *.md <- Team memories
|
|
```
|
|
|
|
### Sync API
|
|
|
|
```
|
|
GET /api/claude_code/team_memory?repo={owner/repo} <- Pull
|
|
PUT /api/claude_code/team_memory?repo={owner/repo} <- Push
|
|
```
|
|
|
|
### Sync Semantics
|
|
|
|
- **Pull**: Server content overwrites local files
|
|
- **Push**: Only uploads keys with different content hashes (delta upload)
|
|
- **Deletes don't propagate**: Local deletions do not delete remote entries
|
|
- **Limits**: Single file max 250KB, upload body max 200KB (batched)
|
|
|
|
### Team vs. Personal Routing Rules
|
|
|
|
In `memoryTypes.ts`, each type has a `<scope>` directive:
|
|
|
|
| Type | Default Scope |
|
|
|------|---------------|
|
|
| user | Always personal |
|
|
| feedback | Personal by default; project-level conventions go to team |
|
|
| project | Leans toward team |
|
|
| reference | Usually team |
|
|
|
|
---
|
|
|
|
## 9. Key Constants Quick Reference
|
|
|
|
```typescript
|
|
// Index file
|
|
ENTRYPOINT_NAME = 'MEMORY.md'
|
|
MAX_ENTRYPOINT_LINES = 200
|
|
MAX_ENTRYPOINT_BYTES = 25_000
|
|
|
|
// Scanning
|
|
MAX_MEMORY_FILES = 200
|
|
FRONTMATTER_MAX_LINES = 30
|
|
|
|
// Path
|
|
AUTO_MEM_DIRNAME = 'memory'
|
|
|
|
// Extraction
|
|
maxTurns = 5 // Forked agent max 5 turns
|
|
|
|
// Retrieval
|
|
Max 5 relevant memories returned
|
|
```
|
|
|
|
---
|
|
|
|
## 10. Data Flow Overview
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────┐
|
|
│ Session Startup │
|
|
│ │
|
|
│ loadMemoryPrompt() │
|
|
│ -> ensureMemoryDirExists() │
|
|
│ -> buildMemoryLines() + MEMORY.md content │
|
|
│ -> Inject into system prompt │
|
|
└────────────────────────┬────────────────────────────┘
|
|
|
|
|
┌─────────────────────────────────────────────────────┐
|
|
│ User Query │
|
|
│ │
|
|
│ findRelevantMemories() │
|
|
│ -> scanMemoryFiles() [scan + frontmatter] │
|
|
│ -> Sonnet selects up to 5 relevant memories │
|
|
│ -> Inject into conversation context │
|
|
│ + freshness warnings │
|
|
└────────────────────────┬────────────────────────────┘
|
|
|
|
|
┌─────────────────────────────────────────────────────┐
|
|
│ Claude's Response │
|
|
│ │
|
|
│ Model may write memories directly │
|
|
│ (following system prompt guidance) │
|
|
│ Or not -> triggers background extraction │
|
|
└────────────────────────┬────────────────────────────┘
|
|
|
|
|
┌─────────────────────────────────────────────────────┐
|
|
│ Background Auto-Extraction │
|
|
│ │
|
|
│ executeExtractMemories() │
|
|
│ -> Mutual exclusion check │
|
|
│ (main agent already wrote? skip) │
|
|
│ -> Build extraction prompt + memory manifest │
|
|
│ -> runForkedAgent() │
|
|
│ [shared cache, restricted tools, 5 turns] │
|
|
│ -> Write new memory files + update MEMORY.md │
|
|
│ -> Notify user │
|
|
└────────────────────────┬────────────────────────────┘
|
|
|
|
|
┌─────────────────────────────────────────────────────┐
|
|
│ Background Memory Consolidation (AutoDream) │
|
|
│ │
|
|
│ executeAutoDream() │
|
|
│ [triggers every 24h + 5 sessions] │
|
|
│ -> Five-gate check │
|
|
│ (toggle/time/throttle/session/lock) │
|
|
│ -> buildConsolidationPrompt() │
|
|
│ -> runForkedAgent() │
|
|
│ [read-only Bash, memory-dir writes only] │
|
|
│ -> Four phases: │
|
|
│ Orient -> Gather -> Consolidate -> Prune │
|
|
│ -> Merge duplicates / fix stale / compress index │
|
|
│ -> Notify user: "Improved N memories" │
|
|
│ │
|
|
│ See 03-autodream.md for details │
|
|
└─────────────────────────────────────────────────────┘
|
|
```
|