Social Memory — who people are
What it does#
Everyone Bea has ever seen gets a tally. Only the people who made themselves matter get a card. That split is what lets her recognise a regular in a chat of thousands without the prompt filling up with strangers.
src/core/skills/social/
├── social.py SocialMemory — the per-batch hook and two tools
└── people.py pure decisions: should_promote, promotion_reason
The storage is in bea.db: identities, roster,
roster_sessions, people, facts.
The two tiers#
A tally (RosterEntry) is a row of counters: how many messages, how much
donated, whether there was ever a 1:1, how many distinct sessions. It carries no
generated content, so it costs one INSERT per message and is affordable for
every chatter that ever types.
A card (PersonCard) is real memory: names across platforms, facts, and how
she feels about them. It is generated, so it is only worth minting for someone
who earned it.
def should_promote(entry) -> bool:
return (
entry.donation_total > 0 # money
or entry.marked_by_bea # she decided to
or entry.had_1on1 # a real 1:1
or entry.session_count >= REGULAR_SESSION_THRESHOLD # a regular (3+)
)
session_count is COUNT(*) over roster_sessions, a join table rather than a
counter — "three distinct sessions" is what makes a regular, and a counter that
can double-count would mint cards for one-time visitors.
The per-batch hook#
context_for(batch) does three things at once, on every batch:
for each author in the batch (skipping the owner):
├─ meta["tallied"]? ──► already counted by a high-volume surface, skip
├─ roster.record(...) the sighting
├─ _maybe_promote(entry) mints a card the moment the rule fires
└─ collect their card
→ "[WHO YOU'RE TALKING TO]\n- **marco** (you: he's alright): builds redstone…"
Capped at MAX_CARDS_INJECTED people so a crowded room cannot take over the
prompt. Cards are injected rather than looked up, so she recognises whoever is
in front of her without spending a tool call to ask.
The tallied flag matters: Twitch counts every message itself,
including the ones the attention gate filtered out. Counting again here would
double them.
Tools#
| Tool | Effect |
|---|---|
remember_person(name, note, attitude) | her own in-character decision. Always persists — if the platform never gave a stable id, a named:<name> identity is synthesized so the card exists anyway |
recall_person(name) | what she knows. Falls back to the raw tally ("seen 12 times across 2 sessions") when there is no card |
link_person(name, speaking_as?) | declares that whoever is talking right now is someone she knows under another name. Links only, never creates — only when they told her so themselves |
remember_person is also armed on written channels — it is the one memory action that
makes sense while she is texting.
Where cards come from#
| Source | When |
|---|---|
| promotion | the rule above fires during a batch |
| donation | immediately, on the webhook — money does not wait for a dreamer |
remember_person | she decided to |
| the profiler | after ~20 of someone's messages, and refreshed rarely |
| the dreamer | overnight, from the whole session |
Every source mints through one function (promote_entry in people.py):
an identity whose display name exactly matches a card links to that card
instead of minting a second one, when either
- the identity shared a session with the card, or
- the card is known only by name (
is_placeholder): every identity on it is a synthesizednamed:<name>, or it has none left. It has no sessions to share, so the first account wearing exactly its name is taken to be that person. Once it has an account behind it, it is no longer a placeholder.
Near matches, and two accounts that never shared a session, stay separate —
two different humans with one first name must not become one card.
repair_duplicate_cards folds existing duplicates at boot on the same
condition. link_person folds the card the speaker was on into the one they
named, once no identity points at it any more.
When two cards do share a name, name lookups answer with the oldest.
Facts are capped per card (MAX_FACTS_STORED) and only the most recent are
shown, so a card that keeps growing never eats the prompt.
Configuration#
"social_memory": { "enabled": true }
No other keys — the thresholds are constants in store.py and people.py
(REGULAR_SESSION_THRESHOLD, MAX_FACTS_STORED, MAX_FACTS_SHOWN), because
they are design decisions rather than settings.