Summary
Sibling of #1391 / #1278, one layer deeper. LIFEOS/TOOLS/MemoryTypes.ts builds storage paths with pathJoin(CLAUDE_ROOT, "LifeOS") (mixed case) while MutationTier.ts and MemoryWriter.ts build their allowlists with "LIFEOS/..." (all caps). The allowlists match on path strings, so on case-sensitive filesystems every Tier-A hot-layer write is rejected:
ETIER_MISMATCH: Type 'memory' declares tier A, but resolved path
~/.claude/LifeOS/USER/PRINCIPAL/PRINCIPAL_MEMORY.md classifies as tier D.
and after the tier layer, MemoryWriter.validatePath() fails the same way (EINVAL_PATH: Path not in allowlist).
Crucially, the #1391 symlink workaround does not help here: LifeOS → LIFEOS fixes file I/O, but path.resolve() does not resolve symlinks, so the string comparison still misses. On macOS (case-insensitive APFS) both bugs are invisible.
Net effect on Linux: the autonomic memory loop runs, the reviewer extracts items, and every memory write is refused — combined with the template bug (filed separately) the memory subsystem is fully inert on Linux installs.
Environment
Ubuntu 24.04, LifeOS v6.0.0, ~/.claude/LifeOS → ~/.claude/LIFEOS symlink in place per #1391.
Repro
bun LIFEOS/TOOLS/MemoryReviewer.ts test → E2E dispatch failure with ETIER_MISMATCH (above).
Fix suggestion (verified locally)
Canonicalize (symlink-resolve) both the allowlist constants and the classify/validate input. For not-yet-existing files, realpath the deepest existing ancestor and re-attach the tail:
function canonicalize(absolutePath: string): string {
let head = absolutePath, tail = "";
while (head !== "/" && !existsSync(head)) {
tail = tail ? pathJoin(basename(head), tail) : basename(head);
head = dirname(head);
}
try {
const real = realpathSync(head);
return tail ? pathJoin(real, tail) : real;
} catch { return absolutePath; }
}
Applied in MutationTier.ts (allowlists + getTier) and MemoryWriter.ts (ALLOWED_FILES + validatePath). Side benefit: a symlink planted inside an allowlisted dir can no longer smuggle a write to its real target, which strengthens the classifier's default-deny posture. All five memory smoke suites pass after the patch (MutationTier 24/24 incl. three new symlink-spelling regression cases).
Longer term: one exported constant (or shared resolve helper, cf. DeployComponents.resolveLifeosDir mentioned in #1391) for the LifeOS dir would remove the dual spelling at the source. Happy to send a PR.
Summary
Sibling of #1391 / #1278, one layer deeper.
LIFEOS/TOOLS/MemoryTypes.tsbuilds storage paths withpathJoin(CLAUDE_ROOT, "LifeOS")(mixed case) whileMutationTier.tsandMemoryWriter.tsbuild their allowlists with"LIFEOS/..."(all caps). The allowlists match on path strings, so on case-sensitive filesystems every Tier-A hot-layer write is rejected:and after the tier layer,
MemoryWriter.validatePath()fails the same way (EINVAL_PATH: Path not in allowlist).Crucially, the #1391 symlink workaround does not help here:
LifeOS → LIFEOSfixes file I/O, butpath.resolve()does not resolve symlinks, so the string comparison still misses. On macOS (case-insensitive APFS) both bugs are invisible.Net effect on Linux: the autonomic memory loop runs, the reviewer extracts items, and every memory write is refused — combined with the template bug (filed separately) the memory subsystem is fully inert on Linux installs.
Environment
Ubuntu 24.04, LifeOS v6.0.0,
~/.claude/LifeOS → ~/.claude/LIFEOSsymlink in place per #1391.Repro
bun LIFEOS/TOOLS/MemoryReviewer.ts test→ E2E dispatch failure withETIER_MISMATCH(above).Fix suggestion (verified locally)
Canonicalize (symlink-resolve) both the allowlist constants and the classify/validate input. For not-yet-existing files, realpath the deepest existing ancestor and re-attach the tail:
Applied in
MutationTier.ts(allowlists +getTier) andMemoryWriter.ts(ALLOWED_FILES+validatePath). Side benefit: a symlink planted inside an allowlisted dir can no longer smuggle a write to its real target, which strengthens the classifier's default-deny posture. All five memory smoke suites pass after the patch (MutationTier 24/24 incl. three new symlink-spelling regression cases).Longer term: one exported constant (or shared resolve helper, cf.
DeployComponents.resolveLifeosDirmentioned in #1391) for the LifeOS dir would remove the dual spelling at the source. Happy to send a PR.