JuliusBrussee/caveman · error
cave_memory_namespace_invalid
cave_memory_namespace_invalid
Error message
cave_memory_namespace_invalid
What it means
Thrown by memoryFilePath() when namespace fails NAMESPACE_PATTERN /^[a-z0-9][a-z0-9_-]{0,95}$/. This is the strictest of the three patterns: lowercase only (no /i flag), must start with a lowercase letter or digit, only letters/digits/underscore/hyphen afterwards, and a 96-character cap. The namespace names the JSON file (<namespace>.json) inside the tenant/agent directory, so the charset prevents both traversal and filename tricks.
Source
Thrown at packages/agent/src/memory-store.ts:62
return process.env.CAVE_AGENT_MEMORY_ROOT ?? join(homedir(), ".caveman", "agent-memory");
}
/**
* The durable file for (tenant, agentId, namespace). The three scoping
* components are validated to a `[a-z0-9_-]`-class charset with no `.` or path
* separator, so no component can traverse out of the memory root.
*/
export function memoryFilePath(
config: MemoryStoreConfig | undefined,
agentId: string,
namespace: string,
): string {
const tenant = config?.tenant ?? "_";
if (tenant !== "_" && !TENANT_PATTERN.test(tenant)) {
throw new Error("cave_memory_tenant_invalid");
}
if (!AGENT_PATTERN.test(agentId)) throw new Error("cave_memory_agent_invalid");
if (!NAMESPACE_PATTERN.test(namespace)) throw new Error("cave_memory_namespace_invalid");
return join(config?.root ?? defaultRoot(), tenant, agentId, `${namespace}.json`);
}
function isMemoryEntry(value: unknown): value is MemoryEntry {
return value !== null && typeof value === "object" &&
typeof (value as { text?: unknown }).text === "string" &&
Number.isSafeInteger((value as { createdAt?: unknown }).createdAt);
}
/** Read the durable entries. A missing or corrupt file is an empty store, never a throw into a run. */
export async function readMemories(filePath: string): Promise<MemoryEntry[]> {
try {
const parsed: unknown = JSON.parse(await readFile(filePath, "utf8"));
return Array.isArray(parsed) ? parsed.filter(isMemoryEntry) : [];
} catch {
return [];
}
}View on GitHub (pinned to 766dce6b13)
Solutions
- Use a lowercase slug: "long-term-memory", "scratch", "facts".
- Normalize before use: ns.toLowerCase().replace(/[^a-z0-9_-]/g, "-").slice(0, 96).
- Check the 96-character cap when generating namespaced keys programmatically.
Example fix
// before memoryFilePath(config, agentId, "LongTermMemory"); // after memoryFilePath(config, agentId, "long-term-memory");
Defensive patterns
Strategy: validation
Validate before calling
const NAMESPACE_PATTERN = /^[a-z0-9][a-z0-9_-]{0,95}$/;
if (!NAMESPACE_PATTERN.test(namespace)) {
throw new TypeError("namespace must be lowercase [a-z0-9_-], start alphanumeric, max 96 chars");
} Type guard
function isValidNamespace(ns: string): boolean {
return /^[a-z0-9][a-z0-9_-]{0,95}$/.test(ns);
} Prevention
- Namespace is lowercase-only — normalize camelCase constants with .toLowerCase().
- Ban dots (file extensions) and slashes in namespace constants.
- Check the 96-character cap when generating namespaced keys.
When it happens
Trigger: namespace "Profile" (uppercase rejected), "profile.v2" (dot), "default/main" (slash), "-notes" (leading hyphen), an empty string, or a namespace longer than 96 characters.
Common situations: camelCase or PascalCase namespace constants ("longTermMemory"); namespaces mirroring file names including extensions; copy-pasting a namespace pattern from the tenant code where uppercase was allowed.
Related errors
- cave_memory_tenant_invalid
- cave_memory_agent_invalid
- caveman agent: tool prefix cave_ is reserved by framework ($
- recordOutcome: values must be a non-empty plain object
- recordOutcome: evidence must be a plain object
AI-assisted analysis of JuliusBrussee/caveman@766dce6b13 (2026-08-18).
Data as JSON: /api/errors/7cfd5683eb19c628.
Report an issue: GitHub.