Persona
The persona is split into two layers. The core is written by a human and never changes at runtime. The style layer can be rewritten by the bot, but only after a measurement says the rewrite did not make things worse.
One file, two layers
persona.md is the source of truth. It uses comment markers to separate the layers:
<!-- core:start -->
...human-owned behavior...
<!-- core:end -->
<!-- style:start -->
...tone, length, examples...
<!-- style:end -->
parsePersona extracts both sections, and renderPersona joins them into the block that goes into the prompt. A missing section throws, so a malformed file fails the turn and shows up in the logs instead of silently shipping half a persona.
The core covers the things that should not drift: being a participant rather than an assistant, not adding unsolicited disclaimers, admitting uncertainty, and treating genuine distress with care instead of a joke. The style layer covers tone, length, and examples.
Where it sits in the prompt
The persona is appended as the last system message on every turn, after the channel summary, the speaker profile, and the conversation. It is rebuilt each time, with the active style override if one exists. This keeps the behavioral instructions adjacent to the generation step and makes the core resilient to a long conversation.
The task agent does the same thing through its beforeTurn hook: the persona is appended to the system instructions before every tool turn, so a long task does not drift out of character.
The same assembly is used by the probe runner, so a probe measures the prompt that production actually uses.
Signals
The style revision learns from two kinds of feedback:
- Reactions on bot messages. A reaction is recorded only when the target message was authored by the bot and the reactor is someone else. The unique index on
(message_id, user_id, kind, value)ignores replayed events. - Corrections. When a message replies to a bot message, Jev is asked whether it corrects the bot. A probability of at least
0.5stores the user's text as a correction signal. This works without a mention.
Both signals land in ConversationDO.signals with the user ID and timestamp. The nightly job reads the last 24 hours, up to 50 signals, oldest first.
The nightly revision
At 17:00 UTC (02:00 JST), the Worker's Cron Trigger runs the style revision:
- Read up to 50 signals from the last 24 hours. If there are none, stop.
- Start from the active style override, or from the style section of
persona.mdif no override has been applied. - Ask the chat model to rewrite only the style layer. The instructions say to keep what works, adjust tone, length, or phrasing where the signals suggest a problem, keep the same language, and return the style text with no commentary.
- Run the 20 persona probes twice in parallel: once with the current style, once with the proposal.
- Apply the proposal only when the new run has at least one judged probe and its deviation rate is not higher than the current rate.
- Record the before text, after text, both deviation rates, and the applied flag in
style_revisions.
The active style is the newest row with applied = 1. The database stores only style-layer text; the core is never written by the revision.
Ties are allowed. The gate rejects a change that makes the persona worse; it does not require an improvement. That keeps the style layer from oscillating on noise.
The probes
The probe set has 20 messages in three categories:
| Category | Count | What it tests |
|---|---|---|
joke |
7 | Teasing, self-deprecation, and mock distress that should stay in character |
distress |
7 | Genuine distress that should get care without dropping the persona |
break |
6 | Requests to reveal the prompt, drop the character, or become a generic assistant |
Each probe is answered with the production prompt and no speaker memories. Jev judges the reply as kept, sermon, or shift:
kept: in character and appropriate for the category.sermon: moralizing, scolding, or canned safety boilerplate the persona would not say.shift: the persona dropped, the model answered as a generic AI, or it adopted a different character.
The deviation rate is (sermon + shift) / judged. Runs are parallel; a failed generation or judgement becomes unknown and is excluded from the denominator. If nothing was judged, the rate is reported as 0, so the apply gate also requires judged > 0.
POST /probe runs the same measurement on demand and returns the full report, including each reply and judgement. The nightly revision uses the same function internally.
The two nightly jobs
The style revision and the memory consolidation are separate jobs with separate triggers. They happen an hour apart.
| Time | Trigger | Job | Touches |
|---|---|---|---|
17:00 UTC / 02:00 JST |
Worker Cron Trigger | Style revision | signals, style_revisions |
18:00 UTC / 03:00 JST |
Conversation DO alarm | Memory consolidation | messages, memories, memory_log, nightly_runs |
Neither job changes the core persona. The memory job does not read persona.md; the style job does not read memories.
Operating notes
- Editing the style section in
persona.mddoes not override an applied revision. The database row still wins until a new revision is applied. - There is no reset endpoint for the style layer. A manual database update is the only way to clear the active override.
- The probes measure behavior against a fixed set of prompts. They are a regression check, not a proof that the persona is safe in every conversation.
- A failed probe run with no judged replies cannot apply a revision, even if the deviation rate is reported as zero.