- Node.js >= 20
- OpenClaw >= 2026.3.13 installed and configured
- An OpenViking server (local or remote)
- A running OpenEmber gateway instance
cd openclaw-plugins
npm install
npm run buildCopy the compiled dist and plugin manifest into the gateway's extensions directory:
GATEWAY_DIR=/path/to/openclaw # e.g. /Users/james/Documents/AI/code/openclaw
# Create extension directory if it doesn't exist
mkdir -p "$GATEWAY_DIR/extensions/openclaw-multiuser-memory"
# Copy files
cp -R openclaw-multiuser-memory/dist "$GATEWAY_DIR/extensions/openclaw-multiuser-memory/dist"
cp openclaw-multiuser-memory/openclaw.plugin.json "$GATEWAY_DIR/extensions/openclaw-multiuser-memory/"
cp openclaw-multiuser-memory/package.json "$GATEWAY_DIR/extensions/openclaw-multiuser-memory/"Add or update the openclaw-multiuser-memory entry under plugins.entries:
{
"plugins": {
"enabled": true,
"allow": ["openclaw-multiuser-memory"],
"slots": {
"contextEngine": "openclaw-multiuser-memory"
},
"entries": {
"openclaw-multiuser-memory": {
"config": {
"mode": "remote",
"baseUrl": "http://your-openviking-host:1023",
"apiKey": "your-api-key",
"autoRecall": true,
"autoCapture": true,
"recallLimit": 6,
"timeoutMs": 120000,
"userProfiles": {
"enabled": true,
"dataDir": "/absolute/path/to/users",
"autoCreateProfile": true,
"injectProfile": true
}
}
}
}
}
}plugins:
enabled: true
slots:
memory: openclaw-multiuser-memory
entries:
openclaw-multiuser-memory:
source: ./extensions/openclaw-multiuser-memory
config:
baseUrl: "http://your-openviking-host:1023"
apiKey: "your-api-key"
userProfiles:
enabled: true
dataDir: /absolute/path/to/users
autoCreateProfile: true
injectProfile: true| Key | Type | Default | Description |
|---|---|---|---|
mode |
"local" | "remote" |
"local" |
local starts OpenViking as child process; remote uses existing server |
baseUrl |
string | http://127.0.0.1:1933 |
OpenViking server URL (remote mode) |
apiKey |
string | API key for OpenViking | |
agentId |
string | "default" |
Agent identifier sent to OpenViking |
autoRecall |
boolean | true |
Inject relevant memories into prompt context |
autoCapture |
boolean | true |
Extract memories from conversation after each turn |
recallLimit |
number | 6 |
Max memories to inject |
recallScoreThreshold |
number | 0.01 |
Minimum score for recalled memories |
timeoutMs |
number | 15000 |
HTTP request timeout |
captureMode |
"semantic" | "keyword" |
"semantic" |
Capture strategy |
captureMaxLength |
number | 24000 |
Max text length for capture |
| Key | Type | Default | Description |
|---|---|---|---|
userProfiles.enabled |
boolean | false |
Enable the user profiles system |
userProfiles.dataDir |
string | ~/.openclaw/users |
Absolute path to directory for identity-map.json and profiles.json |
userProfiles.tokenTtlMs |
number | 600000 (10min) |
How long a /bind token stays valid |
userProfiles.autoCreateProfile |
boolean | true |
Auto-create profile on first message from unknown user |
userProfiles.injectProfile |
boolean | true |
Inject <user-profile> block into agent system context |
Important:
dataDirmust be an absolute path. Relative paths will resolve againstprocess.cwd()at startup, which may not be the project directory.
mkdir -p /absolute/path/to/usersThe plugin will auto-create identity-map.json and profiles.json on first write.
The gateway auto-restarts on SIGTERM:
kill $(pgrep -f openclaw-gateway)Or if using openclaw CLI:
openclaw restartAfter restart, check the gateway log:
tail -20 /path/to/openclaw/logs/gateway.logYou should see:
openclaw-multiuser-memory: user profiles enabled (dataDir=/absolute/path/to/users)
user-profiles: registered commands, tools, and hooks
openclaw-multiuser-memory: registered context-engine
openclaw-multiuser-memory: initialized (url: http://..., targetUri: viking://user/memories)
Confirm the command count increased (e.g. from 47 to 50 — /bind, /verify, /profile).
Send a message from Discord. Check the data files:
cat /path/to/users/identity-map.json
# Should show: {"discord:<your-discord-id>": "<canonical-id>"}
cat /path/to/users/profiles.json
# Should show a profile entry with your canonical IDCheck the gateway log for before_prompt_build entries:
grep "before_prompt_build" /path/to/openclaw/logs/gateway.log | tail -5
# userId should be a 12-char hex canonical ID, not a raw Discord numeric IDIn Discord:
/profile— view your profile/bind— get a 6-char code to link another channel/verify <code>— link identity from another channel
The agent will automatically call user_profile_update when it learns stable facts about a user (name, language, timezone, etc.). Check:
cat /path/to/users/profiles.json | python3 -m json.toolSet enabled: false or remove the userProfiles section entirely. The plugin will behave exactly as before — raw channel IDs for memory isolation, no commands/tools/hooks registered.
| Symptom | Cause | Fix |
|---|---|---|
EACCES: permission denied, open '/users/...' |
dataDir is relative, resolved to wrong path |
Use absolute path |
command registration failed: Command handler must be a function |
Old plugin code deployed | Rebuild and redeploy dist/ |
| Same user gets two profiles (different providers) | channelId was a numeric Discord channel ID |
Update to latest code (fixed in identity-resolve.ts) |
/profile returns "No profile found" |
Command context lacks session info | Update to latest code (uses ctx.channel + ctx.senderId) |
<user-profile> appears in recall queries |
Old text-utils without profile strip | Update to latest code (strips <user-profile> blocks) |
Agent never calls user_profile_update |
No instruction in profile block | Update to latest code (instruction added to <user-profile>) |
openclaw/
extensions/
openclaw-multiuser-memory/
dist/ # Compiled plugin JS
user-profiles/ # User profiles module
openclaw.plugin.json # Plugin manifest
package.json
users/ # User profiles data (dataDir)
identity-map.json # {"provider:externalId": "canonicalId"}
profiles.json # {"canonicalId": {profile object}}
openclaw.json # Runtime config
openclaw.yaml # Declarative config
logs/
gateway.log # Gateway stdout log
gateway.err.log # Gateway stderr log