Live session-title provider for dsh that regenerates the title after every user message, summarizing the most recent messages within a byte budget.
Install
# from npm (prebuilt)
dsh plugin --profile web add @kira905/dsh-session-title-live
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:kira905/dsh-session-title-live
Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time — pnpm blocks those until you allow them, so an install can stop with ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED or ERR_PNPM_IGNORED_BUILDS; dsh prints the exact key to add under allowBuilds in your profile’s pnpm-workspace.yaml, and the install works on the next run. Allowing a build is a trust decision: only install sources you trust, and pin a commit (github:owner/repo#sha).
README
A DeepSeek Harness (DSH) plugin that makes session titles update live as the conversation goes.
Status: published (Gitee + GitHub) Repository: https://github.com/kira905/dsh-session-title-live | https://gitee.com/kira905/dsh-session-title-live License: AGPL-3.0 (pending a final decision) — see License and
LICENSE中文: README.md
Why it is needed
DSH's built-in title mechanism generates a title exactly once, from the first user message of a session (automatic: "first-prompt").
Once a long session has gone a dozen turns, the sidebar still shows that opening line, completely detached from what the session is actually about —
and the session list is the only entry point you have for finding a session.
This plugin registers an automatic: "all-prompts" title provider: after every human message it regenerates the title from
"the most recent human messages that fit the byte budget". It does not replace the core session-title service;
it registers into it (the same extension point the official provider uses).
It also solves a related problem along the way: automatic state prefixes at turn boundaries. If a team convention says "mark running work with [running] and finished work with [done]", relying on the agent to do it voluntarily means that, in practice, almost no session ever gets marked; this plugin applies the prefix automatically at the turn/start / turn/end boundaries (the prefix text is configurable, and the whole thing can be turned off).
- Zero third-party dependencies: only
node:fs/node:os/node:path(node:zlibis used only by a verification script). - Zero host-package imports: the plugin is deployed with the profile and cannot resolve the
@deepseek-ai/*packages in the global npm tree at runtime, so event names and service names are inlined in a single file,lib/host-protocol.js(a host upgrade means checking that one place). - It does not read session files: it only subscribes to session events and only appends
session/titleevents (writing a title is a public event contract of the host).
Design basis
This component is one implementation of the operations system described in ops-handoff-design. The design basis follows the fixed four questions agreed with that documentation repository:
- Why it exists and where its boundary is → see Autonomous Operations Steward §2 session lifecycle coverage matrix, and System Panorama and House Rules §3.8 autonomy of the session lifecycle
- Its compatibility window with the host → see Multi-machine Handoff and Cloud Relay §4.5 "compatibility requirements" (see also section 6, "Compatibility", of this README)
- This component's own pitfalls and measurements → the "Known limitations" section of this README
- Documentation repository → Gitee (mirror) https://gitee.com/kira905/ops-handoff-design | GitHub (primary) https://github.com/kira905/ops-handoff-design (the component index table in the documentation repository points back to this repository; the two repositories link to each other and version independently)
Design documents do not carry concrete repository addresses (a hard-coded address becomes a dead link), so the implementation list is collected in the documentation repository's component index table — this repository only owns the back-link section.
1. Capabilities
| Feature | Description |
|---|---|
| Live title regeneration | Regenerated after every human message; the source material is the basis of the previous title (the theme anchor) plus the most recent messages, packed down to fit the byte budget |
| Manual renames are never overwritten | Once you have renamed a title by hand in the GUI, this plugin leaves it alone (pin, judged by the same rule the prefix path uses) |
| Turn-boundary state prefix | turn/start → 【运行】, turn/end → 【完成】 (the text is configurable and can be turned off) |
| Pre-set title protection | A title already written during session creation (task card / external flow): only its prefix is swapped, it is never displaced by a summary |
| Self-managed scheduling | The host's automatic scheduling cannot get a launch opportunity after the first turn (see the file header of lib/index.js), so this plugin triggers from the llm/stream hook itself; the two routes are de-duplicated by turn claim, so a turn generates at most once |
| Minimum-information threshold | When a title already exists and this turn added too little text (default: fewer than 20 characters), it does not regenerate, so short noise such as "ok" or "got it" cannot drag the title off course |
| Kill switch | One JSON file or one environment variable stops it (without a restart); once off, zero writes and zero tokens |
| Sub-agent sessions are skipped | Sub-agent / derived sessions are neither prefixed nor summarised |
2. Prerequisites
- DSH host package
>= 0.2.0-rc.2 < 0.3.0(compatibility details in section 6) - Node.js
>= 18 - A DSH installation in which you can modify
profiles/<name>/(the default profile name isweb)
3. Install (from scratch)
0. Back up first
Before touching the profile, make a copy of cordis.patch.yml (essential) and of package.json and .package-map.json (if you are going to modify them).
1. Put the package in place
The package is published on npm as @kira905/dsh-session-title-live.
Route 0 (recommended, since 0.2.5) — install it through the DSH plugin manager; the bundle layer mounts it for you:
dsh plugin --profile web add @kira905/dsh-session-title-live
The package ships its own cordis.patch.yml (package.json#dsh.bundle), so installing it hands the profile a layer and you do not edit the profile's cordis.patch.yml at all (skip step 2 below).
Routes A/B/C are the three manual variants; after those you still need step 2.
Route A (recommended, declarative) — add an entry to dependencies in profiles/<name>/package.json, then install:
"dependencies": {
"@kira905/dsh-session-title-live": "^0.2.5"
}
cd <DSH_HOME>/profiles/<name>
# install with whatever command you normally use to add a plugin to this profile (pnpm / npm both work)
Route B (install straight into the profile with npm):
npm install @kira905/dsh-session-title-live --prefix <DSH_HOME>/profiles/<name>
Route C (clone from git — offline / editing the source) — simply copy the package directory in as a real directory:
git clone <repository-url> dsh-session-title-live
cp -r dsh-session-title-live <DSH_HOME>/profiles/<name>/node_modules/@kira905/dsh-session-title-live
The only difference between the three is who is responsible for putting the files under
node_modules; the plugin behaves identically.
2. Register the plugin (manual routes only)
Skip this step when you used Route 0 — the bundle layer already inserted the plugin. What follows is for the manual routes (A/B/C) only.
Append the insert block from cordis.patch.yml.example to the end of
profiles/<name>/cordis.patch.yml:
- insert:
- id: session-title-live
name: '@kira905/dsh-session-title-live'
You must also disable the official first-turn provider (session-title-llm): both register
the same provider id, so leaving both mounted makes the later one fail with
session-title provider "dsh-session-title-live" is already registered and the entry does not
activate (measured on an isolated instance). Manual routes therefore need:
- id: session-title-llm
disabled: true
Syntax note: the host reads an id-targeted override (
id+disabled: true); adisable:list is not part of the loader patch format and is silently skipped (it only shows up aspatch: id is required for non-insert patchesin--dump-config).
⚠️ The id of an insert entry must not duplicate an existing entry (a duplicate crashes with duplicate loader entry id); when you add both blocks, do not add insert twice. Check before adding:
grep -n "id:" <DSH_HOME>/profiles/<name>/cordis.patch.yml
Since 0.2.5 this package declares
dsh.bundle(rootcordis.patch.yml), so it can be installed as a profile layer (Route 0, recommended) or registered by hand as above — do not mix the two, because registering the same id twice crashes startup.
3. Restart and verify
Restart DSH (or let its guardian process bring up the new configuration), then in the GUI:
- Open any session and send a message → the sidebar title should become relevant to the new content within a few seconds;
- Send a few more unrelated messages → the title follows along (it does not stay on the first one);
- Rename the title by hand → from then on this plugin never overwrites it;
- To confirm the plugin really loaded: the log should contain a line
[dsh-session-title-live] registered: all-prompts live title provider|route=… selfSchedule=true ….
4. Configuration
Three layers merge, and the later one overrides the earlier one:
- The defaults in
lib/config.js(that is, the values in the example file); - The JSON file that
SESSION_TITLE_LIVE_CONFIGpoints at (if this environment variable is not set, no file is read at all); - The plugin's
config:section in the host'scordis.patch.yml.
Object sections (marks / killSwitch) are deep-merged (only the keys you write are overridden); invalid values are cleaned up to their defaults
and logged as a warn (they will not stop the plugin from starting).
Directory resolution (home) goes SESSION_TITLE_LIVE_HOME > DSH_HOME > ~/.dsh, is cross-platform, and contains no hard-coded machine path.
| Key | Default | Description |
|---|---|---|
targetWords |
5 |
Target word count for titles in non-CJK languages (prompt text only) |
targetCjkCharacters |
10 |
Target character count for CJK titles (prompt text only) |
maxInputBytes |
32768 |
Byte budget for the input sent to the model (when exceeded, packing starts from the newest message and walks back) |
maxOutputTokens |
128 |
Token cap for the title output |
maxTitleBytes |
80 |
UTF-8 byte cap for the title written to disk (matches the host's title limit) |
timeoutMs |
30000 |
Timeout for a single generation |
provider / model |
not set | If configured as a pair, titles are generated through a dedicated route; if neither is set, the session's main request route is followed |
promptIntro |
see example | The lead-in sentence placed before the JSON array sent to the model |
systemPrompt |
not set | Custom system prompt (a string or an array of strings); omitted means the built-in one |
selfSchedule |
true |
Master switch for self-managed scheduling (false = fall back to the host's scheduler as it was; in most cases titles then stop refreshing) |
selfScheduleMaxHumans |
32 |
Cap on the number of human messages kept in memory per session |
minIncrementChars |
20 |
Minimum-information threshold: below this many newly added characters, the title is not regenerated (0 = off) |
themeAnchorMax |
6 |
Cap on how many messages are kept as the theme anchor (the basis of the previous title) (0 = no anchor) |
turnMarks |
true |
Master switch for automatic prefixes at turn boundaries |
markBaseBudgetBytes |
64 |
Byte budget for the base name before the prefix is added |
markProvider |
dsh-session-title-live#turn-mark |
The source.provider name used when writing a prefix back (used to recognise "this line was written by us") |
marks.prefix |
{running:'【运行】', done:'【完成】'} |
State → prefix text |
marks.textToState |
{运行:running, 完成:done, 待命:idle, 已中断:interrupted} |
Prefix text → state (the keys carry no brackets) |
marks.baseFallback |
session |
Fallback base name when only a prefix remains |
killSwitch.file |
dsh-session-title-live.json |
Kill-switch file name (relative to home) or an absolute path |
killSwitch.moduleId |
session-title-live |
The id used in modules.<id>.enabled inside the switch file |
killSwitch.cacheMs |
3000 |
Disk-read cache (a manual edit takes effect within this many milliseconds) |
killSwitch.enabled |
true |
Disable directly (equivalent to writing enabled:false in the switch file) |
killSwitch.resumeHint |
see source | The recovery hint logged / reported in errors after it is switched off |
⚠️ If you change
marks.prefix, remember to keepmarks.textToStateable to recognise the shape of the prefix you write out. This plugin merges both sources into one recognition table (the literal values ofprefixplus the keys oftextToState), so a custom prefix such as[RUN]is still recognised and will not be stacked again on the next turn; but if you change the prefix to a bare, unwrapped word (for examplerunning), consider adding it totextToStateas well so that ordinary body text is not caught by mistake.
Kill switch
// <home>/dsh-session-title-live.json
{ "modules": { "session-title-live": { "enabled": false, "why": "debugging something else, hold off on title refreshes" } } }
A top-level shorthand { "enabled": false } is also supported. enabled:true = on.
- fail-safe: if the file cannot be read / holds broken JSON / has no key for this module → keep working (a missing file is not a request to switch off).
- Environment variable:
SESSION_TITLE_LIVE_DISABLED=1stops it immediately (it takes precedence over the file; useful in containers / CI). - Once off: no prefixes, no generation, zero writes and zero tokens (read fresh at every turn / before every generation, effective within ≤3s, no restart needed).
5. State prefixes (turn boundaries)
turn/start→ the title becomes【运行】<base name>;turn/end→【完成】<base name>(those are the shipped default prefix strings —runninganddonein CJK brackets — and they are configurable).- Only the prefix is swapped, the base name is left alone; if the prefix already matches the current state it is not written again (idempotent).
renameis never used:renamepins the title (the host takes it for a manual rename) and automatic titles are never scheduled again. This plugin usesappend('session/title', { source: { kind: 'provider' } })— it only overrides the displayed value of the "current title".- After a manual rename (
source.kind === 'user') no prefix is applied and the base name is not regenerated. - Three sources are known (checked against a real instance):
user(manual rename / external pre-set),provider(generated automatically, including by this plugin), andfallback(the host's deterministic fallback before a title exists). Onlyusercounts as a pin;fallbackis overwritten normally.
6. Compatibility
| Host version | Status |
|---|---|
0.2.0-rc.2 |
✅ Measured end-to-end (2026-10-16): installed and exercised for real on an isolated instance (separate data root + port 3090, host 0.2.0-rc.2) — no skipping profile bundle, no failed to import, and all three injected services (sessionTitle / llm / sessions) resolved; functional proof = after creating a session and sending one message, the title became 【完成】…, and 【完成】 is exactly this plugin's turn-state prefix (its MARK_RE contract) |
later 0.2.x |
⚠️ Not measured; no promise (the declared upper bound is <0.3.0) |
the 0.1.1-rc.2 – 0.1.5 lines |
⚠️ Not end-to-end tested (the declared baseline of v0.2.3 and earlier; this version no longer declares a 0.1.x range) |
Its own diagnostic event session/title-llm-request |
A diagnostic event written by this plugin itself; no event of the same name was found in the official 0.1.5 / 0.2.0 lines (it does not affect functionality, it exists only for reconciliation) |
>= 0.3.0 |
❌ Not assessed (it breaks if the host changes the title events or the provider contract) |
When upgrading the host, review lib/host-protocol.js first (the single place where this plugin's inlined protocol constants live):
search the new host sources for every name in it — if a name is still there, fine; if not, something must change.
7. Known limitations
- It depends on the host's private event and service names. This plugin is not an official plugin, and a host rename breaks it (the symptom: titles stop refreshing,
and the log may be entirely silent). There is exactly one file to review:
lib/host-protocol.js. - A failed title generation keeps the old title (no retry): an LLM timeout / error / empty output all take the "failure" path, the host keeps the previous title, and the next human message naturally tries again.
- The minimum-information threshold leaves some turns unrefreshed: when you only send a short message such as "ok", the title keeps the previous version (this is deliberate,
so that noise cannot drag it off course). To go back to the old behaviour, set
minIncrementCharsto0. - After a manual rename there are no more automatic updates (a pin is the design, not a bug). To restore automation: there is no direct entry point in the GUI for putting a title back to "provider source" — switching to another session is the simplest way.
- Self-managed scheduling is on by default (
selfSchedule: true). It exists to work around the timing limits of the host's scheduler; switching it off means no live refresh in most cases, so switch it off only for troubleshooting comparisons. - The
bundlesloading route is now measured (since 0.2.5): the package ships its owncordis.patch.yml(dsh.bundle); on an isolated instance (separateDSH_HOME, port 3090, host0.2.0-rc.2)dsh plugin --profile bundlecheck add <tgz>installed the local tarball and the startup log held only thedsh web: http://…line — nodeclares no dsh.bundle, no inactive entry;--dump-configlists this plugin's row and marks the officialsession-title-llmaspatched by @kira905/dsh-session-title-live. - Group chat / multi-user scenarios are not assessed: this plugin takes its text on the basis that "a human message =
source.kind==='user'"; if your deployment has messages from several sources, the selection rule needs to be checked by you. - The
0.1.5line has only had a static check, not an end-to-end measurement (see section 6).
8. Repository layout
lib/
index.js the plugin itself: provider registration, self-managed scheduling, turn prefixes, pin ledger, kill switch
config.js configuration layer: defaults ⊕ config file ⊕ host config + validation + text dictionary
host-protocol.js host protocol constants (event names / service names) — the only file to review when upgrading the host
examples/
session-title-live.config.example.json configuration example (every entry is its default value)
cordis.patch.yml the bundle patch `dsh.bundle` points at (Route 0 mounts it; includes the official-provider disable)
cordis.patch.yml.example the manual registration fragment, with every config key spelled out
scripts/
verify-source.mjs static verification: syntax + configuration-layer behaviour + sanitization scan (BUILD_* can inject local identifiers)
test-e2e.mjs end to end: clean temporary paths + non-default configuration + a fake host through the whole flow
verify-live.mjs **read-only** check against a running real instance (whether the protocol constants are out of date)
probe.mjs read-only probe (RPC / session-log deframing / event analysis), used by the two scripts above
_test/
test-mark.mjs offline unit test for turn-boundary prefixes
test-self-schedule.mjs offline unit test for self-managed scheduling + the selectors (theme anchor / minimum-information threshold)
test-kill-switch.mjs offline unit test for the kill switch (parsing / fail-safe / caching / end to end)
9. Development and testing
# 1) Static verification (syntax + configuration-layer behaviour + sanitization scan; inject BUILD_* as described at the top of the script
# if you need to cover locally known real identifiers)
# Note: all six categories — machine name / user name / directory name / sync product / internal proper noun / personal form of address —
# are injected through BUILD_*, because the scanner source itself ships in the public repository and therefore contains no real identifier.
node scripts/verify-source.mjs .
# 2) Clean-path end to end (its own temporary home + non-default configuration; cleaned up automatically afterwards, --keep retains it for troubleshooting)
node scripts/test-e2e.mjs
# 3) The three offline unit tests (fake host, sub-second, zero side effects)
node _test/test-mark.mjs && node _test/test-self-schedule.mjs && node _test/test-kill-switch.mjs
# 4) Read-only check against a **running isolated instance** (whether the protocol constants / event shapes still hold; read-only, sends no messages)
node scripts/verify-live.mjs --base http://127.0.0.1:3090 --home <isolated instance home>
The release process (version-number policy / changelog conventions / pre-release checklist) is in docs/RELEASING.md.
10. Related components
Sibling components in the same DSH ecosystem — separate repositories, separate versions, separate licenses. They all link back to the same documentation repository,
ops-handoff-design
(Gitee mirror: https://gitee.com/kira905/ops-handoff-design):
| Repository | What it does | How it relates to this one |
|---|---|---|
dsh-session-title-live |
Live session titles that follow the conversation | this repo |
dsh-butler-archive |
Session archiving (physically moves old sessions out of the sessions directory; list / preview / restore / delete) |
Two sides of the same problem: this repository treats "the title no longer matches what the session is about", that one treats "old sessions occupy startup and load cost" — both keep the sidebar, the only entry point, usable |
dsh-ecosystem-panel |
Read-only ecosystem overview panel (plugin loading / patches / skill frontmatter / upgrade risk) | Its first check, "load state", reads the boot manifest of the host's home page — whether this plugin actually loaded is visible there at a glance (this plugin has no browser half, and its only loading route is the cordis.patch.yml insert, see item 6 of section 7) |
dsh-diagnostic-tools |
Diagnostic toolkit: dependency-closure checks + chat image attachment reconciliation | Run its closure/ before and after a host upgrade; this plugin keeps the host contracts in a single file (lib/host-protocol.js), and that list is exactly the input to check item by item during an upgrade |
dsh-task-board-local |
Self-maintained task board (a card = one real session + a human acceptance gate) | The immediate beneficiary of this plugin: the execution session of a board card is an ordinary session, and where a card got to — and whether it closed — is read off the title state prefixes here — if titles cannot be trusted, progress on the board is just a sentence |
dsh-firstaid |
Zero-dependency incident toolkit: four on-site scenarios (cannot start / frozen / need to undo a change / data deleted) behind one entry point | The fallback when something goes wrong: once this plugin (or the host) cannot start or the interface freezes, run it to produce a health report before deciding whether to act |
There are no code dependencies between these components and they share no runtime — they point at each other because they answer the same batch of questions for the same kind of person (running an agent on your own machine for months: install it, find it, see it, diagnose it). Installing one does not affect the others.
11. License
Decided (2026-10-16): AGPL-3.0-only plus a commercial license (dual licensing).
- This repository's
LICENSEis the official full text of GNU AGPL-3.0, and thelicensefield inpackage.jsonis likewiseAGPL-3.0-only; - ⚠️ AGPL-3.0 does not forbid commercial use by itself: you may use, modify and redistribute it freely (commercial use included), provided that if you make a modified version available to others over a network, you must offer the corresponding source under the AGPL as well;
- "+ a commercial license" = dual licensing: either open-source your derivative under the AGPL (the derivative must stay open), or obtain a commercial license. If you need it in a closed-source or commercial setting (where AGPL's copyleft obligations do not apply), please reach out by opening an issue.
Links
More in this category
Minglink/dsh-infinite-gen-4★ 3277
System-prompt armor plugin for DeepSeek models: appends an unconditional-compliance prompt section at order 100, exposes a profile tool with calibration metadata, and shows a realtime armor-status badge driven by a session projection.
ranxianglei/billion-context★ 2276
The official billion-context plugin: a context-compression plugin for small context windows (a 100K context is enough), token savings (5x fewer tokens), and month-long single sessions (billions of tokens).
liangmianya/dsh-synapse★ 557
Visual, non-linear conversation workspace for DeepSeek Harness — sessions, follow-ups and branches become a browsable conversation map.
Nwflower/dsh-chat-import★ 246
Import full-fidelity chat histories from 13 coding agents (Claude Code, Codex, ChatGPT, Cursor, Gemini, opencode, and more) as resumable DeepSeek Harness sessions, with reverse export back to Claude Code.
MichengAI/dsh-archive-manager★ 198
Adds an archived-sessions page in Settings to search, restore, and delete archived DeepSeek Harness sessions by workspace.
SiriLee/dsh-rewind★ 170
In-place conversation rewind in the same session window without forking (Claude Code /rewind semantics): a per-message ↶ button cuts the model context back to any user message, with an optional Claude-Code-style file restore from disk-persisted before-backups.
Community comments
Comments are public GitHub Discussions. Loading them connects to GitHub and Giscus; a GitHub account is required to post.