openclaw-topic-shift-reset 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,22 +2,53 @@
2
2
 
3
3
  OpenClaw plugin that detects topic shifts and rotates to a fresh session automatically.
4
4
 
5
- ## Quick start config
5
+ ## Why this plugin exists
6
6
 
7
- Most users should only set these fields:
7
+ OpenClaw builds each model call with the current prompt plus session history. As one session accumulates mixed topics, prompts get larger, token usage grows, and context-overflow/compaction pressure increases.
8
+
9
+ This plugin tries to prevent that by detecting topic shifts and rotating to a new session key when confidence is high. In practice, that keeps subsequent turns focused on the new topic, which usually means:
10
+
11
+ - fewer prompt tokens per turn after a shift
12
+ - less stale context bleeding into new questions
13
+ - lower chance of overflow/compaction churn on long chats
14
+ - classifier state persisted across gateway restarts (no cold start after reboot)
15
+
16
+ Does it deliver? Yes for clear topic changes, especially with embeddings enabled and sane defaults. It is not a core patch, so behavior is best-effort: subtle/short messages can be ambiguous, and hook timing means the triggering turn cannot be guaranteed to become the very first persisted message of the new session in every path.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ openclaw plugins install openclaw-topic-shift-reset
22
+ openclaw plugins enable openclaw-topic-shift-reset
23
+ openclaw plugins info openclaw-topic-shift-reset
24
+ ```
25
+
26
+ Add this plugin entry in `~/.openclaw/openclaw.json` (or merge into your existing config):
8
27
 
9
28
  ```json
10
29
  {
11
30
  "plugins": {
31
+ "allow": ["openclaw-topic-shift-reset"],
12
32
  "entries": {
13
33
  "openclaw-topic-shift-reset": {
14
34
  "enabled": true,
15
35
  "config": {
16
36
  "preset": "balanced",
17
- "embeddings": "auto",
18
- "handoff": "summary",
19
- "dryRun": true,
20
- "debug": true
37
+ "embedding": {
38
+ "provider": "auto",
39
+ "timeoutMs": 7000
40
+ },
41
+ "handoff": {
42
+ "mode": "summary",
43
+ "lastN": 6,
44
+ "maxChars": 220
45
+ },
46
+ "softSuspect": {
47
+ "action": "ask",
48
+ "ttlSeconds": 120
49
+ },
50
+ "dryRun": false,
51
+ "debug": false
21
52
  }
22
53
  }
23
54
  }
@@ -25,19 +56,21 @@ Most users should only set these fields:
25
56
  }
26
57
  ```
27
58
 
28
- Then:
59
+ Restart gateway after install/config changes. After restart, `openclaw plugins info openclaw-topic-shift-reset` should show `Status: loaded`.
60
+
61
+ ## Quick start test
29
62
 
30
- 1. Run with `dryRun: true` and `debug: true`.
63
+ 1. Temporarily set `dryRun: true` and `debug: true`.
31
64
  2. Send normal messages on one topic.
32
65
  3. Switch to a clearly different topic.
33
- 4. Watch logs for `classify`, `suspect`, `rotate-hard`/`rotate-soft`, `dry-run rotate`.
66
+ 4. Watch logs for `classify`, `suspect`, `rotate-hard`/`rotate-soft`, and `would-rotate`.
34
67
  5. Set `dryRun: false` when behavior looks good.
35
68
 
36
69
  ## Presets
37
70
 
38
- - `conservative`: fewer resets, more confirmation
39
- - `balanced`: default
40
- - `aggressive`: faster/more sensitive resets
71
+ - `conservative`: fewer resets, more confirmation.
72
+ - `balanced`: default.
73
+ - `aggressive`: faster/more sensitive resets.
41
74
 
42
75
  Default preset internals:
43
76
 
@@ -53,44 +86,43 @@ Default preset internals:
53
86
 
54
87
  ## Embeddings
55
88
 
56
- `embeddings` supports:
89
+ Canonical key: `embedding`.
90
+
91
+ ```json
92
+ {
93
+ "embedding": {
94
+ "provider": "auto",
95
+ "model": "text-embedding-3-small",
96
+ "baseUrl": "https://api.openai.com/v1",
97
+ "timeoutMs": 7000
98
+ }
99
+ }
100
+ ```
101
+
102
+ Provider options:
57
103
 
58
104
  - `auto` (default)
59
105
  - `openai`
60
106
  - `ollama`
61
107
  - `none` (lexical only)
62
108
 
63
- ## Install
109
+ ## Soft suspect clarification
64
110
 
65
- ```bash
66
- openclaw plugins install openclaw-topic-shift-reset
67
- openclaw plugins enable openclaw-topic-shift-reset
68
- openclaw plugins info openclaw-topic-shift-reset
69
- ```
70
-
71
- Add this plugin entry in `~/.openclaw/openclaw.json` (or merge into your existing config):
111
+ When the classifier sees a soft topic-shift signal (`suspect`) but not enough confidence to rotate yet, the plugin can inject one-turn steer context so the model asks a brief clarification question before continuing.
72
112
 
73
113
  ```json
74
114
  {
75
- "plugins": {
76
- "allow": ["openclaw-topic-shift-reset"],
77
- "entries": {
78
- "openclaw-topic-shift-reset": {
79
- "enabled": true,
80
- "config": {
81
- "preset": "balanced",
82
- "embeddings": "auto",
83
- "handoff": "summary",
84
- "dryRun": false,
85
- "debug": false
86
- }
87
- }
88
- }
115
+ "softSuspect": {
116
+ "action": "ask",
117
+ "prompt": "Potential topic shift detected. Ask one short clarification question to confirm the user's new goal before proceeding.",
118
+ "ttlSeconds": 120
89
119
  }
90
120
  }
91
121
  ```
92
122
 
93
- Restart gateway after install/config changes. After restart, `openclaw plugins info openclaw-topic-shift-reset` should show `Status: loaded`.
123
+ - `action`: `ask` (default) or `none`.
124
+ - `prompt`: optional custom steer text.
125
+ - `ttlSeconds`: max age before a pending steer expires.
94
126
 
95
127
  ## Logs
96
128
 
@@ -104,22 +136,18 @@ Use `config.advanced` only if needed. Full reference:
104
136
 
105
137
  - `docs/configuration.md`
106
138
 
107
- Tuning keys must be inside `advanced`; top-level tuning keys are rejected by schema validation.
139
+ ## Upgrade
108
140
 
109
- Common guardrail for short acknowledgments:
141
+ To update to the latest npm release in your OpenClaw instance:
110
142
 
111
- ```json
112
- {
113
- "advanced": {
114
- "minSignalChars": 20,
115
- "minSignalTokenCount": 3,
116
- "minSignalEntropy": 1.2,
117
- "stripEnvelope": true
118
- }
119
- }
143
+ ```bash
144
+ openclaw plugins update openclaw-topic-shift-reset
145
+ openclaw plugins info openclaw-topic-shift-reset
120
146
  ```
121
147
 
122
- This skips classification for very short/low-information messages (for example `ok`, `gracias`, `por favor`).
148
+ Then restart gateway.
149
+
150
+ `0.2.0` is a breaking config release: legacy alias keys were removed. If startup fails validation, migrate to canonical `embedding` and `handoff` objects (see `docs/configuration.md`).
123
151
 
124
152
  ## Local development
125
153
 
@@ -1,15 +1,26 @@
1
1
  # Configuration
2
2
 
3
- ## Recommended public config
3
+ ## Canonical public config
4
4
 
5
- Use this minimal config for normal users:
5
+ This plugin now accepts one canonical key per concept:
6
6
 
7
7
  ```json
8
8
  {
9
9
  "enabled": true,
10
10
  "preset": "balanced",
11
- "embeddings": "auto",
12
- "handoff": "summary",
11
+ "embedding": {
12
+ "provider": "auto",
13
+ "timeoutMs": 7000
14
+ },
15
+ "handoff": {
16
+ "mode": "summary",
17
+ "lastN": 6,
18
+ "maxChars": 220
19
+ },
20
+ "softSuspect": {
21
+ "action": "ask",
22
+ "ttlSeconds": 120
23
+ },
13
24
  "dryRun": false,
14
25
  "debug": false
15
26
  }
@@ -17,16 +28,23 @@ Use this minimal config for normal users:
17
28
 
18
29
  ## Public options
19
30
 
20
- - `enabled`: turn plugin behavior on/off.
31
+ - `enabled`: plugin on/off.
21
32
  - `preset`: `conservative | balanced | aggressive`.
22
- - `embeddings`: `auto | openai | ollama | none`.
23
- - `handoff`: `none | summary | verbatim`.
24
- - `dryRun`: if `true`, logs decisions but never rotates sessions.
25
- - `debug`: if `true`, emits per-message metrics logs.
26
-
27
- ## Built-in presets defaults
28
-
29
- `preset` controls the classifier layer. These are the built-in defaults:
33
+ - `embedding.provider`: `auto | openai | ollama | none`.
34
+ - `embedding.model`: optional model override for selected provider.
35
+ - `embedding.baseUrl`: optional provider base URL override.
36
+ - `embedding.apiKey`: optional explicit API key override.
37
+ - `embedding.timeoutMs`: embedding request timeout.
38
+ - `handoff.mode`: `none | summary | verbatim_last_n`.
39
+ - `handoff.lastN`: number of transcript messages to include in handoff.
40
+ - `handoff.maxChars`: per-message truncation cap in handoff text.
41
+ - `softSuspect.action`: `ask | none`.
42
+ - `softSuspect.prompt`: optional steer text injected on soft-suspect.
43
+ - `softSuspect.ttlSeconds`: expiry for pending soft-suspect steer.
44
+ - `dryRun`: logs would-rotate events without session resets.
45
+ - `debug`: emits per-message classifier diagnostics.
46
+
47
+ ## Built-in preset defaults
30
48
 
31
49
  | Key | conservative | balanced (default) | aggressive |
32
50
  | --- | --- | --- | --- |
@@ -45,41 +63,36 @@ Use this minimal config for normal users:
45
63
 
46
64
  ## Shared defaults
47
65
 
48
- These defaults apply in all presets unless overridden:
49
-
50
- - `handoff`: `summary`
51
- - `handoffLastN`: `6`
52
- - `handoffMaxChars`: `220`
53
- - `embeddings`: `auto`
66
+ - `embedding.provider`: `auto`
54
67
  - `embedding.timeoutMs`: `7000`
55
- - `minSignalChars`: `20`
56
- - `minSignalTokenCount`: `3`
57
- - `minSignalEntropy`: `1.2`
58
- - `stripEnvelope`: `true`
68
+ - `handoff.mode`: `summary`
69
+ - `handoff.lastN`: `6`
70
+ - `handoff.maxChars`: `220`
71
+ - `softSuspect.action`: `ask`
72
+ - `softSuspect.ttlSeconds`: `120`
73
+ - `advanced.minSignalChars`: `20`
74
+ - `advanced.minSignalTokenCount`: `3`
75
+ - `advanced.minSignalEntropy`: `1.2`
76
+ - `advanced.minUniqueTokenRatio`: `0.34`
77
+ - `advanced.shortMessageTokenLimit`: `6`
78
+ - `advanced.embeddingTriggerMargin`: `0.08`
79
+ - `advanced.stripEnvelope`: `true`
80
+ - `advanced.handoffTailReadMaxBytes`: `524288`
59
81
 
60
82
  ## Advanced overrides
61
83
 
62
- The runtime resolves config in this order:
63
-
64
- 1. Built-in preset defaults.
65
- 2. Shared defaults.
66
- 3. `advanced` overrides (only the keys you set).
67
-
68
- Power users can override behavior via `advanced`:
84
+ Advanced keys let you override classifier internals and envelope stripping:
69
85
 
70
86
  ```json
71
87
  {
72
88
  "preset": "balanced",
73
89
  "advanced": {
74
90
  "cooldownMinutes": 3,
75
- "minSignalEntropy": 1.4,
76
- "softConsecutiveSignals": 2,
77
- "softScoreThreshold": 0.7,
78
- "hardScoreThreshold": 0.84,
79
- "handoffLastN": 5,
80
- "embedding": {
81
- "model": "text-embedding-3-small",
82
- "timeoutMs": 7000
91
+ "embeddingTriggerMargin": 0.1,
92
+ "minUniqueTokenRatio": 0.4,
93
+ "stripRules": {
94
+ "dropLinePrefixPatterns": ["^[A-Za-z][A-Za-z _-]{0,30}:\\s*\\["],
95
+ "dropFencedBlockAfterHeaderPatterns": ["^[A-Za-z][A-Za-z _-]{0,40}:\\s*\\([^)]*(metadata|context)[^)]*\\):?$"]
83
96
  }
84
97
  }
85
98
  }
@@ -94,7 +107,14 @@ Advanced keys:
94
107
  - `minSignalChars`
95
108
  - `minSignalTokenCount`
96
109
  - `minSignalEntropy`
110
+ - `minUniqueTokenRatio`
111
+ - `shortMessageTokenLimit`
112
+ - `embeddingTriggerMargin`
97
113
  - `stripEnvelope`
114
+ - `stripRules.dropLinePrefixPatterns`
115
+ - `stripRules.dropExactLines`
116
+ - `stripRules.dropFencedBlockAfterHeaderPatterns`
117
+ - `handoffTailReadMaxBytes`
98
118
  - `softConsecutiveSignals`
99
119
  - `cooldownMinutes`
100
120
  - `softScoreThreshold`
@@ -104,20 +124,29 @@ Advanced keys:
104
124
  - `softNoveltyThreshold`
105
125
  - `hardNoveltyThreshold`
106
126
  - `ignoredProviders`
107
- - `handoff`
108
- - `handoffLastN`
109
- - `handoffMaxChars`
110
- - `embeddings`
111
- - `embedding.provider`
112
- - `embedding.model`
113
- - `embedding.baseUrl`
114
- - `embedding.apiKey`
115
- - `embedding.timeoutMs`
116
127
 
117
- Notes:
128
+ `ignoredProviders` expects canonical provider IDs:
129
+
130
+ - `telegram`, `whatsapp`, `signal`, `discord`, `slack`, `matrix`, `msteams`, `imessage`, `web`, `voice`
131
+ - model/provider IDs like `openai`, `anthropic`, `ollama` (for fallback hook contexts)
132
+
133
+ ## Migration note
118
134
 
119
- - Top-level public keys stay minimal: `enabled`, `preset`, `embeddings`, `handoff`, `dryRun`, `debug`.
120
- - Tuning keys outside `advanced` are rejected by config schema validation.
135
+ Legacy alias keys are not supported in this release. Config validation fails if you use old keys such as:
136
+
137
+ - `embeddings` (top-level)
138
+ - string `handoff` values (top-level)
139
+ - `handoffMode`, `handoffLastN`, `handoffMaxChars`
140
+ - `advanced.embedding`, `advanced.embeddings`, `advanced.handoff*`
141
+ - previous top-level tuning keys
142
+
143
+ ## Runtime persistence
144
+
145
+ Classifier runtime state is persisted automatically under the OpenClaw state directory (`plugins/<plugin-id>/runtime-state.v1.json`).
146
+
147
+ - Persisted: per-session topic history, pending soft-signal windows, topic centroid, rotation dedupe map.
148
+ - Not persisted: transient fast-event dedupe cache.
149
+ - No extra config is required.
121
150
 
122
151
  ## Log interpretation
123
152
 
@@ -133,17 +162,9 @@ Kinds:
133
162
  - `rotate-hard`: immediate reset trigger.
134
163
  - `rotate-soft`: soft signal confirmed.
135
164
 
136
- Reasons:
137
-
138
- - `warmup`
139
- - `stable`
140
- - `cooldown`
141
- - `skip-low-signal`
142
- - `soft-suspect`
143
- - `soft-confirmed`
144
- - `hard-threshold`
145
-
146
165
  Other lines:
147
166
 
148
- - `dry-run rotate`: would have rotated, but `dryRun=true`.
167
+ - `skip-low-signal`: message skipped by hard signal floor (`minSignalChars`/`minSignalTokenCount`).
168
+ - `would-rotate`: `dryRun=true` synthetic rotate event (no reset mutation).
149
169
  - `rotated`: actual session rotation happened.
170
+ - `classify` / `skip-low-signal` / `skip-cross-path-duplicate`: debug-level diagnostics (`debug=true`).
@@ -15,15 +15,75 @@
15
15
  "enum": ["conservative", "balanced", "aggressive"],
16
16
  "default": "balanced"
17
17
  },
18
- "embeddings": {
19
- "type": "string",
20
- "enum": ["auto", "none", "openai", "ollama"],
21
- "default": "auto"
18
+ "embedding": {
19
+ "type": "object",
20
+ "additionalProperties": false,
21
+ "properties": {
22
+ "provider": {
23
+ "type": "string",
24
+ "enum": ["auto", "none", "openai", "ollama"],
25
+ "default": "auto"
26
+ },
27
+ "model": {
28
+ "type": "string"
29
+ },
30
+ "baseUrl": {
31
+ "type": "string"
32
+ },
33
+ "apiKey": {
34
+ "type": "string"
35
+ },
36
+ "timeoutMs": {
37
+ "type": "integer",
38
+ "minimum": 1000,
39
+ "maximum": 30000,
40
+ "default": 7000
41
+ }
42
+ }
22
43
  },
23
44
  "handoff": {
24
- "type": "string",
25
- "enum": ["none", "summary", "verbatim"],
26
- "default": "summary"
45
+ "type": "object",
46
+ "additionalProperties": false,
47
+ "properties": {
48
+ "mode": {
49
+ "type": "string",
50
+ "enum": ["none", "summary", "verbatim_last_n"],
51
+ "default": "summary"
52
+ },
53
+ "lastN": {
54
+ "type": "integer",
55
+ "minimum": 1,
56
+ "maximum": 20,
57
+ "default": 6
58
+ },
59
+ "maxChars": {
60
+ "type": "integer",
61
+ "minimum": 60,
62
+ "maximum": 800,
63
+ "default": 220
64
+ }
65
+ }
66
+ },
67
+ "softSuspect": {
68
+ "type": "object",
69
+ "additionalProperties": false,
70
+ "properties": {
71
+ "action": {
72
+ "type": "string",
73
+ "enum": ["none", "ask"],
74
+ "default": "ask"
75
+ },
76
+ "prompt": {
77
+ "type": "string",
78
+ "default": "Potential topic shift detected. Ask one short clarification question to confirm the user's new goal before proceeding."
79
+ },
80
+ "ttlSeconds": {
81
+ "type": "integer",
82
+ "minimum": 10,
83
+ "maximum": 1800,
84
+ "default": 120
85
+ }
86
+ }
27
87
  },
28
88
  "dryRun": {
29
89
  "type": "boolean",
@@ -79,10 +139,61 @@
79
139
  "maximum": 8,
80
140
  "default": 1.2
81
141
  },
142
+ "minUniqueTokenRatio": {
143
+ "type": "number",
144
+ "minimum": 0,
145
+ "maximum": 1,
146
+ "default": 0.34
147
+ },
148
+ "shortMessageTokenLimit": {
149
+ "type": "integer",
150
+ "minimum": 1,
151
+ "maximum": 40,
152
+ "default": 6
153
+ },
154
+ "embeddingTriggerMargin": {
155
+ "type": "number",
156
+ "minimum": 0,
157
+ "maximum": 0.5,
158
+ "default": 0.08
159
+ },
82
160
  "stripEnvelope": {
83
161
  "type": "boolean",
84
162
  "default": true
85
163
  },
164
+ "stripRules": {
165
+ "type": "object",
166
+ "additionalProperties": false,
167
+ "properties": {
168
+ "dropLinePrefixPatterns": {
169
+ "type": "array",
170
+ "items": {
171
+ "type": "string"
172
+ },
173
+ "default": []
174
+ },
175
+ "dropExactLines": {
176
+ "type": "array",
177
+ "items": {
178
+ "type": "string"
179
+ },
180
+ "default": []
181
+ },
182
+ "dropFencedBlockAfterHeaderPatterns": {
183
+ "type": "array",
184
+ "items": {
185
+ "type": "string"
186
+ },
187
+ "default": []
188
+ }
189
+ }
190
+ },
191
+ "handoffTailReadMaxBytes": {
192
+ "type": "integer",
193
+ "minimum": 65536,
194
+ "maximum": 8388608,
195
+ "default": 524288
196
+ },
86
197
  "softConsecutiveSignals": {
87
198
  "type": "integer",
88
199
  "minimum": 1,
@@ -137,52 +248,6 @@
137
248
  "type": "string"
138
249
  },
139
250
  "default": []
140
- },
141
- "handoff": {
142
- "type": "string",
143
- "enum": ["none", "summary", "verbatim", "verbatim_last_n"]
144
- },
145
- "handoffLastN": {
146
- "type": "integer",
147
- "minimum": 1,
148
- "maximum": 20,
149
- "default": 6
150
- },
151
- "handoffMaxChars": {
152
- "type": "integer",
153
- "minimum": 60,
154
- "maximum": 800,
155
- "default": 220
156
- },
157
- "embeddings": {
158
- "type": "string",
159
- "enum": ["auto", "none", "openai", "ollama"]
160
- },
161
- "embedding": {
162
- "type": "object",
163
- "additionalProperties": false,
164
- "properties": {
165
- "provider": {
166
- "type": "string",
167
- "enum": ["auto", "none", "openai", "ollama"],
168
- "default": "auto"
169
- },
170
- "model": {
171
- "type": "string"
172
- },
173
- "baseUrl": {
174
- "type": "string"
175
- },
176
- "apiKey": {
177
- "type": "string"
178
- },
179
- "timeoutMs": {
180
- "type": "integer",
181
- "minimum": 1000,
182
- "maximum": 30000,
183
- "default": 7000
184
- }
185
- }
186
251
  }
187
252
  }
188
253
  }
@@ -193,13 +258,17 @@
193
258
  "label": "Behavior Preset",
194
259
  "help": "conservative = fewer resets, balanced = default, aggressive = faster resets."
195
260
  },
196
- "embeddings": {
197
- "label": "Embeddings",
198
- "help": "auto uses whichever embedding backend your instance already has configured."
261
+ "embedding": {
262
+ "label": "Embedding Backend",
263
+ "help": "Set one provider config for embeddings used by this plugin."
199
264
  },
200
265
  "handoff": {
201
266
  "label": "Context Handoff",
202
- "help": "summary keeps transitions transparent without copying full transcript."
267
+ "help": "mode=summary is the safest default; verbatim_last_n copies recent transcript lines."
268
+ },
269
+ "softSuspect": {
270
+ "label": "Soft-Suspect Clarification",
271
+ "help": "When a soft topic-shift signal appears, optionally steer the model to ask one clarification question before proceeding."
203
272
  },
204
273
  "dryRun": {
205
274
  "label": "Dry Run",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-topic-shift-reset",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "OpenClaw plugin that detects topic shifts and starts a fresh session automatically.",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -17,6 +17,8 @@
17
17
  "scripts": {
18
18
  "build": "echo 'No build required. OpenClaw loads src/index.ts via jiti.'",
19
19
  "dev": "echo 'No watch build required.'",
20
+ "test": "vitest run",
21
+ "test:watch": "vitest",
20
22
  "prepack": "npm run build"
21
23
  },
22
24
  "keywords": [
@@ -32,6 +34,10 @@
32
34
  "peerDependencies": {
33
35
  "openclaw": ">=2026.2.18"
34
36
  },
37
+ "devDependencies": {
38
+ "@types/node": "^22.13.8",
39
+ "vitest": "^3.0.7"
40
+ },
35
41
  "openclaw": {
36
42
  "extensions": [
37
43
  "./src/index.ts"