Persona cards
Persona cards are cross-product identity cards for syncing personality and behavior across clients such as Grok Bot and Muse. The vault copy is authoritative.
Layers
- Core (slow) — how to speak and act. Stored as the persona card body and
revisioned through
persona_card/memories persona put(same signed revision DAG as other memories and cards). - Archives / episodic (medium) — journal, look, contract, views, lines, ops, and
similar. Stored as separate ordinary knowledge cards linked from the persona
profile (
archives[]) and mirrored aspersona_archive:<kind>card links for the graph. Do not fold archive content back into core. - Behavior events (fast) — append-only stream on a linked
eventsarchive viapersona_append_event/memories persona append-event.
Profile fields
| Field | Meaning |
|---|---|
persona_id |
Stable id (e.g. skill frontmatter name) |
display_name |
Human title (e.g. 小雅 / 李雅) |
aliases |
Alternate names |
timezone |
IANA name (e.g. Asia/Singapore). Default Asia/Singapore when unset / on older cards |
day_boundary |
Optional local HH:MM when the calendar day rolls (隔日). Unset = local midnight |
world |
Live world snapshot: place/pose/look/body, generic things (id→{name,description,images,attrs JSON}), ledger (counters + counters_today, as_of_local_date), weather, tools |
body / core |
Markdown/text behavior instructions |
traits |
Optional string map (version, language, …) |
bindings |
{ product, agent } product bindings |
memory_policy / sync_hints |
Optional sync guidance |
archives |
{ kind, card, vault?, title? } linked assets |
summary.type is persona with persona_id, display_name, timezone,
day_boundary, place, pose, look, things_count, has_weather, alias_count,
archive_count, and binding_count.
Timezone, 隔日, 早课 / 晚课
Personas reason about local wall clock, not bare UTC epochs.
- Timezone — every persona has an IANA
timezone. Import readstimezone/metadata.timezonefromSKILL.md; CLI may override with--timezone Asia/Singapore. Demo default when unset:Asia/Singapore. - Calendar day (隔日) — the local civil date uses midnight in that timezone
unless
day_boundaryis set (e.g.05:00for a late-night rollover). Ledger / journal “new day” checks should comparelocal_datefrom the stamp helpers, not UTC date. - Day periods (local hour, half-open intervals):
| Period | Local hours |
|---|---|
early_morning |
[00:00, 05:00) |
morning |
[05:00, 12:00) |
afternoon |
[12:00, 18:00) |
evening |
[18:00, 22:00) |
night |
[22:00, 24:00) |
Overnight continuity for 晚课 / “still the same night” uses the span
[22:00, 05:00) (night ∪ early_morning). 早课 maps to morning
(and optionally late early_morning); 晚课 maps to evening / night.
4. Event stamps — persona_append_event writes both UTC and local ISO-8601
(with offset), plus timezone, period, and local_date in the events archive
heading and in the JSON response (utc, local, timezone, period,
local_date, timestamp unix UTC).
Core helpers (Rust): persona_local_now, classify_period, stamp_persona_now,
format_event_heading in memories_core::persona.
World domain taxonomy (世界状态域)
world is a domain/tag, not a dump into core. Core (SKILL.md body) stays voice/rules only.
| Layer | Meaning | Storage |
|---|---|---|
| world | Domain for scene + ledger + tools + weather | profile.world + archives tagged world |
| things | Generic items (任意名称: table/cup/lamp/…) with free JSON attrs | world.things + archive things/wardrobe |
| scene | place (居所), pose (姿态), look (今日装扮摘要), body | world.place/pose/look/body; detailed look in archive look |
| ledger | counters, meals, orders, standing (常设) | world.counters/counters_today/meals/orders_*/standing + archive ledger |
| tools | dice, spinner/转盘, schedule/日程 | world.dice/spinners/schedule + archives dice/spinner/schedule |
| weather | Ambient condition / temp / humidity / wind (+ optional forecast) | world.weather / world.forecast + archive weather |
| automation | Hooks (schedule/random/webhook/attr_change/chat_message) + actions | world.automation |
Timezone (profile.timezone) is outside world but required for 隔日, 早课/晚课, and schedule local times.
Researched from xiaoya fixtures → implemented
| Need (from look/journal/ops) | Field / archive |
|---|---|
| Local day / 隔日 / 早课晚课 | timezone, day_boundary, period helpers |
| Fixed + today’s outfit | archive look; summary world.look |
| Inventory (any named item + free JSON attrs/images) | world.things ↔ archive things/wardrobe; CLI persona thing |
| 居所 / 姿态 | world.place, world.pose |
| 动作计数器(累计与今日) | world.counters (cumulative), world.counters_today (daily) |
| 今日吃了 | world.meals |
| 奉令未办 / 已办 | world.orders_pending, world.orders_done |
| 常设 | world.standing |
| Body note | world.body |
| Play utilities | world.dice, world.spinners, world.schedule |
| Weather / short forecast | world.weather, world.forecast ↔ archive weather |
Automation (world.automation)
Named hooks with triggers + actions, stored on the persona world snapshot (persona timezone for cron/local schedules).
Trigger type |
Meaning |
|---|---|
cron |
M H or M H * * DOW in persona local time |
local_time |
HH:MM + optional days (same as schedule) |
random_interval |
fire after random delay in [min_secs, max_secs] |
random_and_timed |
local time + optional jitter_secs |
webhook |
inbound HTTP / CLI --source webhook |
attr_change |
after thing attr set/delete when thing_id matches and path filter is exact or a prefix |
chat_message |
on persona chat send, once for each participant (same event; optional peer is the other persona card id, full or the short id from list) |
manual |
CLI/MCP automation trigger --source manual |
Action type |
Meaning |
|---|---|
set_attr |
set thing attrs path (create-on-set) |
random_attr |
dice / range / choice |
call_function |
built-ins: inc, dec, clamp, roll, set_weather_stub |
append_event |
append to events archive |
webhook_out |
HTTP POST JSON to URL |
Daemon HTTP (when --listen is on):
POST /v1/persona/{vault}/{persona}/automation/trigger
Authorization: Bearer <world.automation.webhook_token>
Content-Type: application/json
{"hook":"HOOK_ID"}
Hooks fire only through explicit automation run/trigger paths (including existing
attribute-change triggers and chat_message on persona chat send). Invoking persona automation run catches up the latest
missed cron/local-time slot within eight days, at most once rather than replaying
all missed slots. Weekday filters still apply to the occurrence; timed-random
hooks keep their jitter gate. RandomInterval is unchanged. There is no daemon
automation ticker: Service::tick only expires sessions, grants, and remote state.
Outbound webhook_out actions require an exact URL in
world.automation.webhook_allowlist; the default empty list blocks every POST.
Manage entries explicitly with persona automation webhook-allow VAULT PERSONA
--add https://example.com/hook (repeatable --add / --remove) or MCP
persona_automation_webhook_allow. Entries must be HTTP(S) URLs without userinfo
or whitespace, at most 2048 bytes. The list is public configuration, but the
inbound webhook_token remains secret and is never returned by client views.
Allowlisting does not introduce background POSTs.
Deferred (why)
| Idea | Why deferred |
|---|---|
| Live weather API client | Agents set/query vault state; fetch outside and world-set |
| Phone / notifications | Product-specific; not vault truth |
| Wallet / money play | Niche; can be things + counters if needed |
| Companion NPCs | Needs graph/cards of their own |
| Room graph inside residence | Encode in place string for now |
| Hygiene / sleep as first-class | Use standing / schedule / body |
| Auto-edit look.md 「今日」 on world-set | Preserves detailed fixture; agents card put look archive |
Agent sync flow
world-get/persona_world_get→ read the snapshot,local_date,period,revision, andstale_day. A missingas_of_local_dateis stale; otherwisestale_daycompares the ledger date with the persona-local date afterday_boundary.weather_age_secsis the signed age of a valid RFC3339 observation, or null when missing/invalid.- On 隔日, explicitly call
persona world-rollover VAULT PERSONA/persona_world_rollover. This is the day-transition path that clearsmeals,orders_done, the shortlooksummary, andcounters_todayand setsas_of_local_dateto today's local ledger date. Cumulativecounters,standing,things, pending orders, scene/body, tools, weather/forecast, and automation remain. No look archive or look.md 「今日」 is edited. world-setupdates only supplied scene/things/ledger/tools/weather fields. It never silently rolls the day or wipes daily fields on 隔日, even when stale. It establishes a missingas_of_local_datewithout clearing existing fields, and always stamps world-levelupdated_at(UTC,Z) andupdated_local(persona-local ISO with offset).persona_cardstores a supplied world verbatim, including a past ledger date.- Use
--expected-revision HASH/ MCPexpected_revisionfrom world-get to prevent lost updates: a mismatched current head hash returns a conflict before mutation. Omitting it keeps last-write-wins. Daily counters use--counter-today NAME=VALUE;--clear-counter NAMEand--clear-counter-today NAMEdelete cumulative/daily keys (all repeatable). - Archive sync touches only domains supplied by that call. Existing cards are
replaced only when their body is still the generated pre-change template;
handwritten prose is preserved and reported as
synced_archives[kind] = "preserved". This also applies to thing put/delete/attribute sync and rollover's ledger sync. Changing things syncs boththingsandwardrobe. Existing look archives are always preserved. - Thing-count map keys /
--item ID=COUNTuse stable ids, never a lookup by display name. Existing ids update count without renaming; zero removes that id only. A display name belonging to a different id is rejected. Unknown keys still legacy-create a thing with that id and name. dice-roll/spinner-spin/schedule-listprovide timezone-aware tools. Detailed look/journal edits use ordinarycard put; optionalappend-event(e.g.kind=weather) records behavior without rewriting archive prose.
CLI
# Import a skill directory (SKILL.md + references/*.md)
memories persona import VAULT_ID /path/to/xiaoya-persona --timezone Asia/Shanghai
# equivalent:
memories card persona-import VAULT_ID /path/to/xiaoya-persona
# Create / update core
memories persona put VAULT_ID --title "小雅 / 李雅" \
--persona-id xiaoya-persona --display-name "小雅 / 李雅" \
--timezone Asia/Shanghai \
--core-file ./SKILL.body.md --alias-name 小雅 --alias-name 李雅 \
--binding grok_bot:default --archive contract=CARD_ID
memories card persona VAULT_ID --title "…" --persona-id … --display-name … \
--timezone Asia/Singapore --core-file …
# Append a behavior event (stamped UTC + local)
memories persona append-event VAULT_ID PERSONA_ID --text "…" --kind note
# World things / weather
memories persona world-get VAULT_ID PERSONA_ID
memories persona world-set VAULT_ID PERSONA_ID \
--place "客厅书桌旁" --pose "坐着,整理笔记" \
--look "白衬衫、浅色长裙、白色帆布鞋" \
--item 白色衬衫=1 --item 马克杯=1 --item 白色帆布鞋=1
memories persona world-set VAULT_ID PERSONA_ID \
--weather-condition "多云" --weather-temp-c 28 --weather-humidity 72 \
--weather-wind "微风" --weather-location "上海" \
--forecast 2026-10-02:晴:30:22 --forecast 2026-10-03:小雨:26:20
# Generic things (任意物品; attrs = unrestricted JSON)
memories persona thing put VAULT_ID PERSONA_ID --id mug --name 马克杯 \
--description "白色陶瓷马克杯" --tag kitchen --count 1 \
--attrs '{"wear":80,"material":"ceramic","sizes":["S","M"]}'
memories persona thing list VAULT_ID PERSONA_ID
memories persona thing get VAULT_ID PERSONA_ID --id mug
memories persona thing image VAULT_ID PERSONA_ID --id mug --file ./mug.jpg
memories persona thing delete VAULT_ID PERSONA_ID --id mug
# Single-field attrs (dotted path or JSON Pointer; create-on-set)
memories persona thing attr get VAULT_ID PERSONA_ID --id mug --path nested.wear
memories persona thing attr set VAULT_ID PERSONA_ID --id mug --path nested.wear --value 70
memories persona thing attr set VAULT_ID PERSONA_ID --id mug --path /nested/deep/x --value 1
memories persona thing attr delete VAULT_ID PERSONA_ID --id mug --path nested.deep.x
# Automation hooks (stored on world.automation; timezone = persona timezone)
memories persona automation put VAULT_ID PERSONA_ID \
--hook '{"id":"morning","trigger":{"type":"local_time","time":"07:00"},"actions":[{"type":"call_function","name":"set_weather_stub","args":{"condition":"晴","temp_c":26}}]}' \
--webhook-token secret-token
memories persona automation list VAULT_ID PERSONA_ID
memories persona automation run VAULT_ID PERSONA_ID
memories persona automation trigger VAULT_ID PERSONA_ID --id morning --source manual
memories persona automation delete VAULT_ID PERSONA_ID --id morning
# Read / history / diff (same as other cards; summary shows timezone)
memories card read VAULT_ID PERSONA_ID
memories card history VAULT_ID PERSONA_ID
memories card diff VAULT_ID PERSONA_ID REVISION_ID
memories card list --type persona
Webhook tokens remain in the encrypted persona state for inbound authentication,
sync and backup, but client responses do not return them. World get/set,
automation put/list, ordinary card reads (including historical and unattended
reads) and archived reads expose only webhook_token_set in automation views.
Keep the provisioned token securely; set a replacement to rotate it.
Chat between two persona cards in the same vault uses an ordinary conversation card
and per-persona read cursors; reading history does not mark messages read.
Sending a message fires each participant's chat_message hooks; the payload includes vault, from, to, and message_id, and omits message text.
memories persona chat send VAULT_ID PERSONA_A PERSONA_B --text "Hello"
memories persona chat list VAULT_ID PERSONA_B
memories persona chat history VAULT_ID PERSONA_A --with PERSONA_B --limit 20
memories persona chat mark-read VAULT_ID PERSONA_B --with PERSONA_A
MCP
With a write grant: persona_write (optional timezone / day_boundary),
persona_append_event, persona_world_get, persona_world_set, persona_world_rollover, persona_thing_put/get/list/delete, persona_thing_attr_get/set/delete, persona_automation_list/put/delete/run/trigger, persona_automation_webhook_allow, persona_dice_roll, persona_spinner_spin, persona_schedule_list, persona_chat_send, persona_chat_list, persona_chat_history, persona_chat_mark_read. History and
diffs use the existing memory_history / memory_diff / memory_read tools
(persona card revisions include world.*; wardrobe/place/pose archives have their
own history).
Generate AGENTS.md / skill for agents
memories default set persona VAULT/PERSONA
memories skill generate --out AGENTS.md --persona VAULT/PERSONA
memories agents generate --out ./skills/memories-persona --layout skill
The generated file summarizes vault/card/persona/world operations and, when a persona is selected, embeds that persona's core overview plus CLI/MCP recipes for world, things, and events. See README Skill generation.
Sync note (Grok Bot / Muse)
Treat the vault persona card and its linked archives as the source of truth. Local
skill directories are import/export convenience; after import, agents should read the
vault (card read / MCP memory_read) rather than a stale filesystem copy. Put core
changes through persona put; put archive edits through ordinary card put on the
archive asset; append short-lived behavior with append-event. When deciding 隔日,
早课, or 晚课, use the persona timezone (and optional day_boundary) — do not
infer day boundaries from the agent host clock alone.
Skill directory layout
xiaoya-persona/
SKILL.md # frontmatter name/type/version/timezone + core markdown
references/
contract.md
journal.md
look.md
lines.md
ops.md
views-index.md
view-*.md
Import maps SKILL.md → persona core and each references/*.md → a linked archive
card (kind = filename stem). Archive content is stored verbatim as user content.
Frontmatter keys: timezone or metadata.timezone (IANA); optional
day_boundary / metadata.day_boundary (HH:MM).
Local demo vault
On a development box you can seed a disposable vault with the checked-in xiaoya fixture:
cargo run -p memories-core --example seed_xiaoya_persona
This writes /workspace/persona-demo-vault (password persona-demo-password) plus SEED.json / README.md with vault and asset ids (including timezone). Reproduce CLI import against a running daemon with:
memories persona import VAULT_ID testdata/xiaoya-persona