Multi-user broke the MEMORY.md write path silently. /var/lib/kai/memory/MEMORY.md is single-global, owned by the service user mode 644. When Kai spawns inner Claude as a non-service os_user via sudo -H -u <os_user>, the subprocess can read the file but cannot write it. The subprocess reports this in-session as "I can't write to the memory file ... (owned by the kai user, I'm running as <os_user>)".
Mem0 facts and chat history are already per-user (user_id=chat_id filters at memory.py:380/949/1038, history at history/<chat_id>/). MEMORY.md is the only plaintext memory surface still single-global.
Scope
Move MEMORY.md to DATA_DIR/memory/<chat_id>/MEMORY.md, mirroring the history layout.
backend.py:226 - build memory_path from chat_id, same scoping as history_dir on the line below.
main.py:93 _bootstrap_memory - becomes lazy per-chat-id bootstrap invoked from the per-message path, not at startup. Startup cannot know the full chat_id set.
install.py migration - existing memory/MEMORY.md relocates to the primary operator's chat_id dir (first user in users.yaml) on next sudo make install. One-time, idempotent.
install.py:1715 ownership - each memory/<chat_id>/ subdir chowned to that user's os_user, resolved from users.yaml. mem0_history.db and qdrant/ stay service-owned (the service writes them, not the subprocess).
Out of scope
- Mem0/Qdrant: already per-user by code-layer filter; filesystem ownership unchanged.
history/<chat_id>/: already per-user on disk.
- Cross-user memory sharing: not a feature; each user's MEMORY.md is theirs alone.
Tests
_build_context reads memory/<chat_id>/MEMORY.md for the given chat_id; does not read other users' files.
- Bootstrap creates the per-chat dir with correct ownership on first message.
- Migration moves legacy
memory/MEMORY.md to the primary chat_id dir and leaves no stub.
- Two chat_ids get independent MEMORY.md files; writes to one do not appear in the other.
Acceptance
Inner Claude running as any os_user can write its own chat's MEMORY.md. A user's notes do not appear in another user's context.
Multi-user broke the MEMORY.md write path silently.
/var/lib/kai/memory/MEMORY.mdis single-global, owned by the service user mode 644. When Kai spawns inner Claude as a non-serviceos_userviasudo -H -u <os_user>, the subprocess can read the file but cannot write it. The subprocess reports this in-session as "I can't write to the memory file ... (owned by the kai user, I'm running as<os_user>)".Mem0 facts and chat history are already per-user (
user_id=chat_idfilters atmemory.py:380/949/1038, history athistory/<chat_id>/). MEMORY.md is the only plaintext memory surface still single-global.Scope
Move MEMORY.md to
DATA_DIR/memory/<chat_id>/MEMORY.md, mirroring the history layout.backend.py:226- buildmemory_pathfromchat_id, same scoping ashistory_diron the line below.main.py:93 _bootstrap_memory- becomes lazy per-chat-id bootstrap invoked from the per-message path, not at startup. Startup cannot know the full chat_id set.install.pymigration - existingmemory/MEMORY.mdrelocates to the primary operator's chat_id dir (first user inusers.yaml) on nextsudo make install. One-time, idempotent.install.py:1715ownership - eachmemory/<chat_id>/subdir chowned to that user'sos_user, resolved fromusers.yaml.mem0_history.dbandqdrant/stay service-owned (the service writes them, not the subprocess).Out of scope
history/<chat_id>/: already per-user on disk.Tests
_build_contextreadsmemory/<chat_id>/MEMORY.mdfor the given chat_id; does not read other users' files.memory/MEMORY.mdto the primary chat_id dir and leaves no stub.Acceptance
Inner Claude running as any
os_usercan write its own chat's MEMORY.md. A user's notes do not appear in another user's context.