Voice
Auto Session Title
OpenClaw plugin that automatically labels sessions with a short title generated from the first user prompt.
Install
npm install #
Configuration Example
{
"plugins": {
"load": {
"paths": ["/ABSOLUTE/PATH/TO/auto-session-title"]
}
}
}
README
# Auto Session Title — OpenClaw plugin
Automatically gives your OpenClaw sessions short, human‑readable titles based on
the first user prompt. Instead of technical identifiers
(`agent:main:main`, `session:local:7f3a9c…`) your session list shows things like
**“Session naming in OpenClaw”** or **“Avalonia UI binding issue”**.
Titles are always generated in **English**, regardless of the conversation
language.
The plugin works entirely through the official OpenClaw plugin API — it does not
modify the core and never edits `sessions.json` by hand.
## Features
- Auto‑labels a session after its first successful agent run.
- Never overwrites a manually set (or previously generated) label, unless you
opt in via `overwriteExisting`.
- One LLM call per session at most — no extra cost once a label exists.
- Sends only a minimal, sanitized slice of the conversation to the model;
strips code/logs and redacts obvious secrets before sending.
- Rejects junk titles (technical ids, secrets, raw JSON, too short).
- Optional cheaper/dedicated model just for titles.
## How it works
```
User sends the first prompt
↓
Agent finishes the run → `agent_end` hook
↓
Plugin checks whether the session already has a label (if yes → stop)
↓
It extracts a minimal, cleaned slice of the conversation
↓
It calls the model (api.runtime.llm.complete) asking for a JSON title
↓
It validates and cleans the result (sanitize-title)
↓
It stores the title as the session `label`
(api.runtime.agent.session.patchSessionEntry)
```
A title is generated **once per session**: when a label already exists, the
plugin performs no LLM call at all (unless you enable `overwriteExisting`).
## Requirements
- OpenClaw `>= 2026.6.0` (verified on `2026.6.10`).
- Node.js `>= 22` (for building and running tests).
## Installation
### 1. Get the plugin and build it
```bash
git clone https://github.com/daropotter/auto-session-title.git
cd auto-session-title
npm install # installs TypeScript + OpenClaw types (devDependencies)
npm run build # compiles src/ → dist/
```
After building, the plugin directory contains: `openclaw.plugin.json`,
`index.js` (a re‑export of `dist/index.js`), and the compiled `dist/`.
### 2. Point OpenClaw at the plugin directory
OpenClaw discovers the plugin by its `openclaw.plugin.json`. Add the plugin
directory to `plugins.load.paths` in your `openclaw.json`:
```json
{
"plugins": {
"load": {
"paths": ["/ABSOLUTE/PATH/TO/auto-session-title"]
}
}
}
```
> You can also point at a parent directory that holds several plugins — OpenClaw
> scans subdirectories for `openclaw.plugin.json`.
### 3. Enable the plugin and grant conversation access
The plugin reads conversation content, so it **requires explicit opt‑in** via
`hooks.allowConversationAccess`. Without it, the OpenClaw host will not deliver
message content to the `agent_end` hook and the plugin does nothing. This is an
OpenClaw security gate, not a plugin setting.
```json
{
"plugins": {
"entries": {
"auto-session-title": {
"enabled": true,
"hooks": {
"allowConversationAccess": true
},
"config": {
"maxTitleChars": 60,
"overwriteExisting": false,
"minMessages": 1
}
}
}
}
}
```
### 4. Restart OpenClaw
After a restart, new sessions are labeled automatically once the first agent run
completes.
## Configuration
All fields go under `plugins.entries["auto-session-title"].config`:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `maxTitleChars` | number | `60` | Maximum title length (10–200). |
| `minMessages` | number | `1` | Minimum number of user messages before a title is generated. |
| `overwriteExisting` | boolean | `false` | Whether to overwrite an existing label. |
| `includeAssistantReply` | boolean | `true` | Include the first assistant reply as extra context. |
| `maxContextChars` | number | `4000` | Character cap on the context sent to the model (200–20000). |
| `model` | string | — | Optional cheaper model ref (see below). |
| `temperature` | number | `0.3` | Model temperature (0–2). |
| `purpose` | string | `auto-session-title.generate` | Audit label passed to `llm.complete`. |
| `liveRefresh` | boolean | `true` | After labeling, emit `sessions.changed` so the Control UI updates the title immediately (no click). Best‑effort. |
Unknown/invalid values are ignored (fall back to defaults) and numeric fields
are clamped to their valid ranges.
### Cheaper/dedicated model for titles (optional)
By default the plugin uses the model configured for the agent. To use a cheaper
model just for titles, the host must allow it via `llm.allowModelOverride` +
`llm.allowedModels`, and you set `config.model`:
```json
{
"plugins": {
"entries": {
"auto-session-title": {
"llm": {
"allowModelOverride": true,
"allowedModels": ["openai/gpt-4.1-mini"]
},
"config": {
"model": "openai/gpt-4.1-mini"
}
}
}
}
}
```
## Instant UI refresh (`liveRefresh`)
The Control UI updates session titles from `sessions.changed` events. The
official host path is `sessions.patch` → `emitSessionsChanged` (full session row
snapshot, `reason: "patch"`). A bare `patchSessionEntry` from the plugin SDK
does **not** emit that broadcast, so the sidebar keeps the old title until the
next `sessions.list` reload (e.g. switching chats).
When `liveRefresh` is enabled (the default), the plugin:
1. **Tries in-process `sessions.patch`** through the host gateway dispatcher
(same code path as a manual label edit in the UI). This persists the label
and broadcasts `sessions.changed` in one step.
2. **Falls back** to `patchSessionEntry` plus a lifecycle `sessions.changed`
emit (with `sessionId` when available) if the dispatcher is unreachable.
Both mechanisms are **best-effort and fully defensive**: host chunks are detected
at runtime from the gateway `dist/` directory; any failure only disables the
live refresh — the title write still succeeds. Set `liveRefresh: false` to skip
refresh attempts entirely.
Log line after labeling: `persist=gateway|store`, `liveRefresh=true|false`,
`via=gateway-patch|lifecycle|none`.
## Security & privacy
- Only a **minimal slice** of the conversation is sent to the model (the first
prompt and optionally the first reply), truncated to `maxContextChars`.
- Code blocks and log lines are stripped, and detected secrets (API keys,
tokens, JWTs, passwords, private keys) are redacted before sending.
- A title that looks like a technical id, a secret, raw JSON, or is too short is
rejected and never stored.
- Conversation access requires an explicit `hooks.allowConversationAccess: true`.
## Development
```bash
npm run build # compile TypeScript → dist/
npm run typecheck # tsc --noEmit (checks compatibility with the OpenClaw SDK)
npm test # build + node --test (unit and integration tests)
npm run clean # remove dist/
```
Tests run against the compiled `dist/` and do not require a running OpenClaw
instance:
- `sanitize-title` — title cleanup and validation,
- `config` — configuration normalization,
- `messages` — context extraction and secret redaction,
- `title-generator` — prompt building and parsing the model response,
- `session-labeler` — reading/writing the label,
- `integration` — the full `agent_end` hook flow against the built plugin entry.
## Architecture
```
auto-session-title/
├── openclaw.plugin.json # plugin manifest (id, configSchema, uiHints)
├── package.json
├── tsconfig.json
├── index.js # entry: re-export of dist/index.js
├── src/
│ ├── index.ts # registers the agent_end hook, orchestration
│ ├── config.ts # config normalization + defaults
│ ├── messages.ts # context extraction/cleanup
│ ├── title-generator.ts # prompt + model call + JSON parsing
│ ├── session-labeler.ts # read/write the session label
│ └── sanitize-title.ts # title validation and cleanup
├── test/ # node --test suites
└── dist/ # build output (generated)
```
OpenClaw APIs used (verified on `[email protected]`):
- `definePluginEntry` from `openclaw/plugin-sdk/core`,
- the `api.on("agent_end", (event, ctx) => …)` hook,
- `api.runtime.agent.session.getSessionEntry` / `patchSessionEntry`,
- `api.runtime.llm.complete(...)`.
## Compatibility note
Hook names, event shapes, and import paths can differ between OpenClaw versions.
The plugin checks at runtime that the APIs it needs (`session`, `llm.complete`)
are available and, if they are missing, quietly skips labeling (logging a
`warn`) instead of breaking the agent run. After an SDK upgrade, run
`npm run typecheck` to catch changes.
## License
[MIT](LICENSE) © Daro Potter
voice
Comments
Sign in to leave a comment