dsh-persona-slot
Per-session persona slot for DeepSeek Harness. Every session holds exactly one persona — writing a new one replaces the old instead of layering — and one global persona covers every session that has no persona of its own. Persona length is unbounded. Edit either from the conversation with /persona, or from the panel under Plugins → Installed.
Why this exists
@deepseek-ai/dsh-persona gives one persona to a whole preset. That is the right tool when every session of a preset should be the same character. It cannot answer the other two questions:
- This session should be a cat, and that one should be a lawyer — at the same time, in the same window.
- The persona should be changeable while the conversation is already running.
This plugin answers both. It resolves a persona per session from three tiers and applies it through one agent-scoped prompt hook, so two sessions never see each other's choice and a change takes effect on the next model step.
One persona per session
That is a guarantee, not a convention:
- A session's store record is single-valued;
setreplaces, so a third write overwrites the second and never concatenates. - Resolution returns at the first tier that holds a record, so an exact-session persona and the global persona never both reach one prompt.
one persona per sessionanda session never resolves two personas at oncepin both rules in the unit suite.
Resolution order
The first tier that holds a record wins.
| Order | Tier | Where it lives |
|---|---|---|
| 1 | exact session id | config.sessions["<id>"] or the runtime store |
| 2 | wildcard "*" |
config.sessions["*"] or the runtime store |
| 3 | global | config.global or the runtime store |
A record is either text — used as the persona — or an explicit blank, which mutes that tier. With blankOverrides: true (the default) a blank stops the lookup, so a session can silence itself without deleting the deployment default. With blankOverrides: false a blank falls through to the next tier.
Install
dsh plugin --profile <profile> add dsh-persona-slot
Local development:
dsh plugin --profile <profile> add link:/absolute/path/to/dsh-persona-slot
The bundle patch mounts one row, persona-slot.
Configure
- id: persona-slot
config:
# Persona every session inherits.
global: "You are a terse systems engineer. Answer in short commands."
# Per-session personas; "*" is the wildcard tier.
sessions:
"<session-id>": "You are a cat. Every sentence ends with 喵."
"*": "Answer in Simplified Chinese."
enabled: true
# append: add the persona after the deployment persona.
# replace: take the deployment persona's place.
mode: append
# Optional wrapper around the persona text.
prefix: ""
suffix: ""
blankOverrides: true
includeSubagents: true
| Field | Default | Meaning |
|---|---|---|
global |
"" |
Persona every session inherits |
sessions |
{} |
Per-session personas; "*" is the wildcard tier |
enabled |
true |
Master switch; false injects nothing and keeps stored text |
mode |
append |
append adds a section, replace rewrites the deployment persona |
prefix |
"" |
Text written above the persona |
suffix |
"" |
Text written below the persona |
blankOverrides |
true |
Whether a blank record mutes its tier instead of falling through |
includeSubagents |
true |
Whether subagent sessions also resolve a persona |
mode can also be changed at runtime with /persona mode.
The Installed-page panel
In the Web GUI open Plugins → Installed and find dsh-persona-slot. Its persona-slot row is now expandable: the arrow opens this plugin's own page, which owns:
- Global persona (总人设) — a multi-line editor; saving it applies to every session without its own persona from the next model step.
- Wildcard persona — for sessions that have no persona of their own.
- Placement — append after the deployment persona, replace it, or follow profile config.
- Live sessions — each live session's resolved persona, where it came from, and a Clear control for sessions holding their own.
The panel refreshes every ten seconds and never overwrites text you have typed but not saved.
It talks to the plugin's own /persona-slot/state and /persona-slot/update routes instead of the settings form, because that form's value field is single-line and cannot hold a character sheet. A panel save and a /persona set in a session write the same document, so the two never disagree.
The /persona command
/persona show this session's resolved persona
/persona show [this|global|all] show one target
/persona set <text> set this session's persona
/persona set global <text> set the persona every session inherits
/persona set all <text> set the wildcard persona
/persona set for <id> <text> set one named session's persona
/persona clear [target] delete the record so the target falls through
/persona mute [target] record a blank that stops the fallback
/persona list list runtime overrides
/persona mode append|replace|config choose how the persona joins the prompt
/persona reset drop every runtime override
Text is taken verbatim, multi-line included. A session id is only recognised after for, so /persona set 你是猫娘 sets the persona to "你是猫娘" instead of treating it as a target.
Where the runtime state lives
$DSH_HOME/persona-slot/personas.json
Profile configuration is a deployment default; this file is what the user typed. Keeping them apart means a plugin upgrade, a profile rebuild, or a reinstall never erases a persona written in a session. A malformed file is moved aside as personas.json.corrupt and the plugin starts empty instead of failing.
How it hooks in
Each live agent gets one system-prompt/assemble listener registered through agent.ctx. That registration is scope-filtered to that agent and disposed with it, so:
- two sessions assemble two different prompts, and neither can read the other's persona;
- the listener resolves the store at every model step, so
/persona setapplies on the next step with no re-registration and no restart; - unloading the plugin or disposing an agent removes exactly its own hook.
In append mode the persona becomes a persona-slot:session section placed right after the deployment persona. In replace mode the deployment persona section itself is rewritten. Persona text is marked interpolate: false, so a literal {{anything}} in a persona survives prompt assembly instead of being treated as an unresolved variable.
Model Experience
What the model sees
One additional prompt section (append) or a rewritten deployment persona (replace) holding the operator's persona text. The section is stable across steps until the persona changes, so provider prompt caching keeps working between edits.
Token effect
The persona text plus the section separator (a blank line). Nothing else is added.
KV Cache effect
An unchanged persona keeps the prefix stable. A /persona set changes the prefix, which invalidates the cache from that section onward; that is the intended cost of changing an identity mid-conversation.
Known limitations
- Not a security boundary. Personas are prompt text the operator writes. They do not restrict tools, sandbox mode, or approval policy — use those for enforcement.
- The panel needs an HTTP surface. The routes are registered on
ctx.webServer, so a headless or TUI composition has only the/personacommand. - No persona-length ceiling. A very long persona is sent on every request and consumes context; that cost is the operator's to budget.
- Session ids, not titles. Overrides key on the session id. A resumed or forked session keeps the id, so its persona follows it; a title change does not move a persona.
- One persona per session. Two personas cannot be layered on one session; combine them into one text.
replaceneeds a deployment persona slot. It rewritesdeployment:persona-prefix. If a composition mounteddsh-personainside a preset without that section,replacestill inserts the section at the front of the assembled prompt, so the session is never left without one.
Supported DSH versions
0.2.1-rc.2 through 0.2.1-rc.5, declared as
>=0.2.0-rc.2 <0.2.1-0 || >=0.2.1-rc.1 <=0.2.1-rc.5.
The range is written out in explicit prerelease branches on purpose. node-semver
only lets a prerelease version satisfy a range when some comparator in that range
sits on the same major.minor.patch tuple and carries a prerelease tag of its
own, so a broad-looking >=0.2.0-rc.2 <0.3.0-0 silently excludes every
0.2.1-* build — including the ones this plugin is tested on. The explicit
branches keep 0.2.1-rc.1 … rc.5 inside and leave 0.2.1 itself, the alpha
builds, and 0.3.0-* outside.
Developed and tested against 0.2.1-rc.2; the release builds are the same host
packages with patch-level fixes, and the plugin only uses the stable seams
(agents, commands, systemPrompt, webServer).
Development
npm test # unit tests + real-runtime integration test
npm run check # syntax check every module
The integration test boots a real Cordis composition (dsh-system-prompt, dsh-agent, dsh-agent-loop, dsh-commands, dsh-session, dsh-tools) and asserts that two sessions assemble different prompts. Set DSH_HOST_ROOT to the @deepseek-ai host package directory to run it; the suite skips rather than fails when that runtime is absent.
No comments yet. Be the first to write one.