JuliusBrussee/caveman · error

cave_memory_tenant_invalid

cave_memory_tenant_invalid

Error message

cave_memory_tenant_invalid

What it means

`memoryFilePath` builds the on-disk path `<root>/<tenant>/<agentId>/<namespace>.json` for the memory store and validates each path segment against strict patterns to prevent traversal. The `tenant` from `MemoryStoreConfig` defaults to "_" but, when set to anything else, must match `/^[a-z0-9][a-z0-9_-]{0,127}$/i`. Invalid tenants (empty after non-underscore, containing `.`/`/`/`\`, starting with `-`/`_`, or longer than 128 chars) throw this error.

Solutions

  1. Normalize the tenant to the allowed charset: alphanumeric, underscore, hyphen, max 128 chars, starting with a letter or digit — e.g. `tenant: "acme-org"` for "acme.org".
  2. Slugify before passing: `tenant.replace(/[^a-zA-Z0-9_-]/g, "-").slice(0, 128).replace(/^[-_]+/, "") || "_"`.
  3. Omit `tenant` (or set "_") if you do not need multi-tenant isolation.

Example fix

// before
createMemoryStore({ tenant: "acme.org" }); // dot rejected
// after
const slug = (t) => t.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/^[-_]+/, "").slice(0, 128) || "_";
createMemoryStore({ tenant: slug("acme.org") }); // "acme-org"
Defensive patterns

Strategy: validation

Validate before calling

const TENANT_RE = /^[a-z0-9][a-z0-9_-]{0,127}$/i;
function safeTenant(raw) {
  if (raw === undefined || raw === "_") return "_";
  const t = String(raw).replace(/[^a-zA-Z0-9_-]/g, "-").replace(/^[-_]+/, "").slice(0, 128);
  if (!t || !TENANT_RE.test(t)) throw new TypeError(`tenant invalid after slugify: ${raw}`);
  return t;
}
createMemoryStore({ tenant: safeTenant(cfg.tenant) });

Type guard

const isValidTenant = (t) => t === "_" || (typeof t === "string" && /^[a-z0-9][a-z0-9_-]{0,127}$/i.test(t));

Try / catch

try {
  const store = createMemoryStore({ tenant: cfg.tenant });
} catch (e) {
  if (e.message === "cave_memory_tenant_invalid") {
    throw new Error(`Tenant '${cfg.tenant}' not allowed; use [a-z0-9_-] only, <=128 chars, no dots/slashes`, { cause: e });
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling `createMemoryStore({ tenant: "acme.org" })` (dot not allowed), `tenant: "../escape"`, `tenant: ""` combined with a truthy check path, `tenant: "-team"` (must start alphanumeric), or a tenant longer than 128 characters.

Common situations: Using an email address, URL, or org domain as the tenant; injecting a path segment containing slashes or dots to organize tenants hierarchically; passing a full tenant key from another system without slugifying it first.

Understand the failure class

Background: "invalid id" errors: invalid identifier format — why libraries reject IDs before lookup, and how to fix them — this error's family across 37 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/92b1bac51d7eee72. Report an issue: GitHub.

Appendix: source

Thrown at packages/agent/src/memory-store.ts:59

const NAMESPACE_PATTERN = /^[a-z0-9][a-z0-9_-]{0,95}$/;

function defaultRoot(): string {
  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 {

View on GitHub (pinned to 3ee70a1026)