agentMemory.ts
tools/AgentTool/agentMemory.ts
178
Lines
5853
Bytes
6
Exports
7
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file is part of the tool layer, which means it describes actions the system can perform for the user or model.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with tool-system, planner-verifier-agents, memory-layers. It contains 178 lines, 7 detected imports, and 6 detected exports.
Important relationships
Detected exports
AgentMemoryScopegetAgentMemoryDirisAgentMemoryPathgetAgentMemoryEntrypointgetMemoryScopeDisplayloadAgentMemoryPrompt
Keywords
scopememoryagentclaudeagent-memoryprojectagenttypejoinuserlocal
Detected imports
path../../bootstrap/state.js../../memdir/memdir.js../../memdir/paths.js../../utils/cwd.js../../utils/git.js../../utils/path.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
import { join, normalize, sep } from 'path'
import { getProjectRoot } from '../../bootstrap/state.js'
import {
buildMemoryPrompt,
ensureMemoryDirExists,
} from '../../memdir/memdir.js'
import { getMemoryBaseDir } from '../../memdir/paths.js'
import { getCwd } from '../../utils/cwd.js'
import { findCanonicalGitRoot } from '../../utils/git.js'
import { sanitizePath } from '../../utils/path.js'
// Persistent agent memory scope: 'user' (~/.claude/agent-memory/), 'project' (.claude/agent-memory/), or 'local' (.claude/agent-memory-local/)
export type AgentMemoryScope = 'user' | 'project' | 'local'
/**
* Sanitize an agent type name for use as a directory name.
* Replaces colons (invalid on Windows, used in plugin-namespaced agent
* types like "my-plugin:my-agent") with dashes.
*/
function sanitizeAgentTypeForPath(agentType: string): string {
return agentType.replace(/:/g, '-')
}
/**
* Returns the local agent memory directory, which is project-specific and not checked into VCS.
* When CLAUDE_CODE_REMOTE_MEMORY_DIR is set, persists to the mount with project namespacing.
* Otherwise, uses <cwd>/.claude/agent-memory-local/<agentType>/.
*/
function getLocalAgentMemoryDir(dirName: string): string {
if (process.env.CLAUDE_CODE_REMOTE_MEMORY_DIR) {
return (
join(
process.env.CLAUDE_CODE_REMOTE_MEMORY_DIR,
'projects',
sanitizePath(
findCanonicalGitRoot(getProjectRoot()) ?? getProjectRoot(),
),
'agent-memory-local',
dirName,
) + sep
)
}
return join(getCwd(), '.claude', 'agent-memory-local', dirName) + sep
}
/**
* Returns the agent memory directory for a given agent type and scope.
* - 'user' scope: <memoryBase>/agent-memory/<agentType>/
* - 'project' scope: <cwd>/.claude/agent-memory/<agentType>/
* - 'local' scope: see getLocalAgentMemoryDir()
*/
export function getAgentMemoryDir(
agentType: string,
scope: AgentMemoryScope,
): string {
const dirName = sanitizeAgentTypeForPath(agentType)
switch (scope) {
case 'project':
return join(getCwd(), '.claude', 'agent-memory', dirName) + sep
case 'local':
return getLocalAgentMemoryDir(dirName)
case 'user':
return join(getMemoryBaseDir(), 'agent-memory', dirName) + sep
}
}
// Check if file is within an agent memory directory (any scope).
export function isAgentMemoryPath(absolutePath: string): boolean {
// SECURITY: Normalize to prevent path traversal bypasses via .. segments
const normalizedPath = normalize(absolutePath)
const memoryBase = getMemoryBaseDir()
// User scope: check memory base (may be custom dir or config home)
if (normalizedPath.startsWith(join(memoryBase, 'agent-memory') + sep)) {
return true
}
// Project scope: always cwd-based (not redirected)
if (
normalizedPath.startsWith(join(getCwd(), '.claude', 'agent-memory') + sep)
) {
return true
}
// Local scope: persisted to mount when CLAUDE_CODE_REMOTE_MEMORY_DIR is set, otherwise cwd-based
if (process.env.CLAUDE_CODE_REMOTE_MEMORY_DIR) {
if (
normalizedPath.includes(sep + 'agent-memory-local' + sep) &&
normalizedPath.startsWith(
join(process.env.CLAUDE_CODE_REMOTE_MEMORY_DIR, 'projects') + sep,
)
) {
return true
}
} else if (
normalizedPath.startsWith(
join(getCwd(), '.claude', 'agent-memory-local') + sep,
)
) {
return true
}
return false
}
/**
* Returns the agent memory file path for a given agent type and scope.
*/
export function getAgentMemoryEntrypoint(
agentType: string,
scope: AgentMemoryScope,
): string {
return join(getAgentMemoryDir(agentType, scope), 'MEMORY.md')
}
export function getMemoryScopeDisplay(
memory: AgentMemoryScope | undefined,
): string {
switch (memory) {
case 'user':
return `User (${join(getMemoryBaseDir(), 'agent-memory')}/)`
case 'project':
return 'Project (.claude/agent-memory/)'
case 'local':
return `Local (${getLocalAgentMemoryDir('...')})`
default:
return 'None'
}
}
/**
* Load persistent memory for an agent with memory enabled.
* Creates the memory directory if needed and returns a prompt with memory contents.
*
* @param agentType The agent's type name (used as directory name)
* @param scope 'user' for ~/.claude/agent-memory/ or 'project' for .claude/agent-memory/
*/
export function loadAgentMemoryPrompt(
agentType: string,
scope: AgentMemoryScope,
): string {
let scopeNote: string
switch (scope) {
case 'user':
scopeNote =
'- Since this memory is user-scope, keep learnings general since they apply across all projects'
break
case 'project':
scopeNote =
'- Since this memory is project-scope and shared with your team via version control, tailor your memories to this project'
break
case 'local':
scopeNote =
'- Since this memory is local-scope (not checked into version control), tailor your memories to this project and machine'
break
}
const memoryDir = getAgentMemoryDir(agentType, scope)
// Fire-and-forget: this runs at agent-spawn time inside a sync
// getSystemPrompt() callback (called from React render in AgentDetail.tsx,
// so it cannot be async). The spawned agent won't try to Write until after
// a full API round-trip, by which time mkdir will have completed. Even if
// it hasn't, FileWriteTool does its own mkdir of the parent directory.
void ensureMemoryDirExists(memoryDir)
const coworkExtraGuidelines =
process.env.CLAUDE_COWORK_MEMORY_EXTRA_GUIDELINES
return buildMemoryPrompt({
displayName: 'Persistent Agent Memory',
memoryDir,
extraGuidelines:
coworkExtraGuidelines && coworkExtraGuidelines.trim().length > 0
? [scopeNote, coworkExtraGuidelines]
: [scopeNote],
})
}