openclaw-topic-shift-reset 0.1.0 → 0.2.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,23 +2,48 @@
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
+
15
+ 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.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ openclaw plugins install openclaw-topic-shift-reset
21
+ openclaw plugins enable openclaw-topic-shift-reset
22
+ openclaw plugins info openclaw-topic-shift-reset
23
+ ```
24
+
25
+ Add this plugin entry in `~/.openclaw/openclaw.json` (or merge into your existing config):
8
26
 
9
27
  ```json
10
28
  {
11
29
  "plugins": {
30
+ "allow": ["openclaw-topic-shift-reset"],
12
31
  "entries": {
13
32
  "openclaw-topic-shift-reset": {
14
33
  "enabled": true,
15
34
  "config": {
16
- "enabled": true,
17
35
  "preset": "balanced",
18
- "embeddings": "auto",
19
- "handoff": "summary",
20
- "dryRun": true,
21
- "debug": true
36
+ "embedding": {
37
+ "provider": "auto",
38
+ "timeoutMs": 7000
39
+ },
40
+ "handoff": {
41
+ "mode": "summary",
42
+ "lastN": 6,
43
+ "maxChars": 220
44
+ },
45
+ "dryRun": false,
46
+ "debug": false
22
47
  }
23
48
  }
24
49
  }
@@ -26,19 +51,21 @@ Most users should only set these fields:
26
51
  }
27
52
  ```
28
53
 
29
- Then:
54
+ Restart gateway after install/config changes. After restart, `openclaw plugins info openclaw-topic-shift-reset` should show `Status: loaded`.
55
+
56
+ ## Quick start test
30
57
 
31
- 1. Run with `dryRun: true` and `debug: true`.
58
+ 1. Temporarily set `dryRun: true` and `debug: true`.
32
59
  2. Send normal messages on one topic.
33
60
  3. Switch to a clearly different topic.
34
- 4. Watch logs for `classify`, `suspect`, `rotate-hard`/`rotate-soft`, `dry-run rotate`.
61
+ 4. Watch logs for `classify`, `suspect`, `rotate-hard`/`rotate-soft`, and `would-rotate`.
35
62
  5. Set `dryRun: false` when behavior looks good.
36
63
 
37
64
  ## Presets
38
65
 
39
- - `conservative`: fewer resets, more confirmation
40
- - `balanced`: default
41
- - `aggressive`: faster/more sensitive resets
66
+ - `conservative`: fewer resets, more confirmation.
67
+ - `balanced`: default.
68
+ - `aggressive`: faster/more sensitive resets.
42
69
 
43
70
  Default preset internals:
44
71
 
@@ -54,23 +81,26 @@ Default preset internals:
54
81
 
55
82
  ## Embeddings
56
83
 
57
- `embeddings` supports:
84
+ Canonical key: `embedding`.
85
+
86
+ ```json
87
+ {
88
+ "embedding": {
89
+ "provider": "auto",
90
+ "model": "text-embedding-3-small",
91
+ "baseUrl": "https://api.openai.com/v1",
92
+ "timeoutMs": 7000
93
+ }
94
+ }
95
+ ```
96
+
97
+ Provider options:
58
98
 
59
99
  - `auto` (default)
60
100
  - `openai`
61
101
  - `ollama`
62
102
  - `none` (lexical only)
63
103
 
64
- ## Install locally
65
-
66
- ```bash
67
- openclaw plugins install --link ~/Projects/openclaw-topic-shift-reset
68
- openclaw plugins enable openclaw-topic-shift-reset
69
- openclaw plugins info openclaw-topic-shift-reset
70
- ```
71
-
72
- Restart gateway after install/config changes.
73
-
74
104
  ## Logs
75
105
 
76
106
  ```bash
@@ -83,35 +113,23 @@ Use `config.advanced` only if needed. Full reference:
83
113
 
84
114
  - `docs/configuration.md`
85
115
 
86
- Tuning keys must be inside `advanced`; top-level tuning keys are rejected by schema validation.
116
+ ## Upgrade
87
117
 
88
- Common guardrail for short acknowledgments:
118
+ To update to the latest npm release in your OpenClaw instance:
89
119
 
90
- ```json
91
- {
92
- "advanced": {
93
- "minSignalChars": 20,
94
- "minSignalTokenCount": 3,
95
- "minSignalEntropy": 1.2,
96
- "stripEnvelope": true
97
- }
98
- }
120
+ ```bash
121
+ openclaw plugins update openclaw-topic-shift-reset
122
+ openclaw plugins info openclaw-topic-shift-reset
99
123
  ```
100
124
 
101
- This skips classification for very short/low-information messages (for example `ok`, `gracias`, `por favor`).
125
+ Then restart gateway.
126
+
127
+ `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`).
102
128
 
103
129
  ## Local development
104
130
 
105
131
  No build step is required. OpenClaw loads `src/index.ts` via jiti.
106
132
 
107
- ## Publish
108
-
109
- ```bash
110
- cd ~/Projects/openclaw-topic-shift-reset
111
- npm publish
112
- npm view openclaw-topic-shift-reset version --userconfig "$(mktemp)"
113
- ```
114
-
115
133
  ## Known tradeoff (plugin-only)
116
134
 
117
135
  This plugin improves timing with fast path + fallback, but cannot guarantee 100% that the triggering message becomes the first persisted message of the new session without core pre-session hooks.
@@ -1,15 +1,22 @@
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
+ },
13
20
  "dryRun": false,
14
21
  "debug": false
15
22
  }
@@ -17,16 +24,20 @@ Use this minimal config for normal users:
17
24
 
18
25
  ## Public options
19
26
 
20
- - `enabled`: turn plugin behavior on/off.
27
+ - `enabled`: plugin on/off.
21
28
  - `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:
29
+ - `embedding.provider`: `auto | openai | ollama | none`.
30
+ - `embedding.model`: optional model override for selected provider.
31
+ - `embedding.baseUrl`: optional provider base URL override.
32
+ - `embedding.apiKey`: optional explicit API key override.
33
+ - `embedding.timeoutMs`: embedding request timeout.
34
+ - `handoff.mode`: `none | summary | verbatim_last_n`.
35
+ - `handoff.lastN`: number of transcript messages to include in handoff.
36
+ - `handoff.maxChars`: per-message truncation cap in handoff text.
37
+ - `dryRun`: logs would-rotate events without session resets.
38
+ - `debug`: emits per-message classifier diagnostics.
39
+
40
+ ## Built-in preset defaults
30
41
 
31
42
  | Key | conservative | balanced (default) | aggressive |
32
43
  | --- | --- | --- | --- |
@@ -45,41 +56,34 @@ Use this minimal config for normal users:
45
56
 
46
57
  ## Shared defaults
47
58
 
48
- These defaults apply in all presets unless overridden:
49
-
50
- - `handoff`: `summary`
51
- - `handoffLastN`: `6`
52
- - `handoffMaxChars`: `220`
53
- - `embeddings`: `auto`
59
+ - `embedding.provider`: `auto`
54
60
  - `embedding.timeoutMs`: `7000`
55
- - `minSignalChars`: `20`
56
- - `minSignalTokenCount`: `3`
57
- - `minSignalEntropy`: `1.2`
58
- - `stripEnvelope`: `true`
61
+ - `handoff.mode`: `summary`
62
+ - `handoff.lastN`: `6`
63
+ - `handoff.maxChars`: `220`
64
+ - `advanced.minSignalChars`: `20`
65
+ - `advanced.minSignalTokenCount`: `3`
66
+ - `advanced.minSignalEntropy`: `1.2`
67
+ - `advanced.minUniqueTokenRatio`: `0.34`
68
+ - `advanced.shortMessageTokenLimit`: `6`
69
+ - `advanced.embeddingTriggerMargin`: `0.08`
70
+ - `advanced.stripEnvelope`: `true`
71
+ - `advanced.handoffTailReadMaxBytes`: `524288`
59
72
 
60
73
  ## Advanced overrides
61
74
 
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`:
75
+ Advanced keys let you override classifier internals and envelope stripping:
69
76
 
70
77
  ```json
71
78
  {
72
79
  "preset": "balanced",
73
80
  "advanced": {
74
81
  "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
82
+ "embeddingTriggerMargin": 0.1,
83
+ "minUniqueTokenRatio": 0.4,
84
+ "stripRules": {
85
+ "dropLinePrefixPatterns": ["^[A-Za-z][A-Za-z _-]{0,30}:\\s*\\["],
86
+ "dropFencedBlockAfterHeaderPatterns": ["^[A-Za-z][A-Za-z _-]{0,40}:\\s*\\([^)]*(metadata|context)[^)]*\\):?$"]
83
87
  }
84
88
  }
85
89
  }
@@ -94,7 +98,14 @@ Advanced keys:
94
98
  - `minSignalChars`
95
99
  - `minSignalTokenCount`
96
100
  - `minSignalEntropy`
101
+ - `minUniqueTokenRatio`
102
+ - `shortMessageTokenLimit`
103
+ - `embeddingTriggerMargin`
97
104
  - `stripEnvelope`
105
+ - `stripRules.dropLinePrefixPatterns`
106
+ - `stripRules.dropExactLines`
107
+ - `stripRules.dropFencedBlockAfterHeaderPatterns`
108
+ - `handoffTailReadMaxBytes`
98
109
  - `softConsecutiveSignals`
99
110
  - `cooldownMinutes`
100
111
  - `softScoreThreshold`
@@ -104,20 +115,21 @@ Advanced keys:
104
115
  - `softNoveltyThreshold`
105
116
  - `hardNoveltyThreshold`
106
117
  - `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
118
 
117
- Notes:
119
+ `ignoredProviders` expects canonical provider IDs:
118
120
 
119
- - Top-level public keys stay minimal: `enabled`, `preset`, `embeddings`, `handoff`, `dryRun`, `debug`.
120
- - Tuning keys outside `advanced` are rejected by config schema validation.
121
+ - `telegram`, `whatsapp`, `signal`, `discord`, `slack`, `matrix`, `msteams`, `imessage`, `web`, `voice`
122
+ - model/provider IDs like `openai`, `anthropic`, `ollama` (for fallback hook contexts)
123
+
124
+ ## Migration note
125
+
126
+ Legacy alias keys are not supported in this release. Config validation fails if you use old keys such as:
127
+
128
+ - `embeddings` (top-level)
129
+ - string `handoff` values (top-level)
130
+ - `handoffMode`, `handoffLastN`, `handoffMaxChars`
131
+ - `advanced.embedding`, `advanced.embeddings`, `advanced.handoff*`
132
+ - previous top-level tuning keys
121
133
 
122
134
  ## Log interpretation
123
135
 
@@ -133,17 +145,8 @@ Kinds:
133
145
  - `rotate-hard`: immediate reset trigger.
134
146
  - `rotate-soft`: soft signal confirmed.
135
147
 
136
- Reasons:
137
-
138
- - `warmup`
139
- - `stable`
140
- - `cooldown`
141
- - `skip-low-signal`
142
- - `soft-suspect`
143
- - `soft-confirmed`
144
- - `hard-threshold`
145
-
146
148
  Other lines:
147
149
 
148
- - `dry-run rotate`: would have rotated, but `dryRun=true`.
150
+ - `skip-low-signal`: message skipped by hard signal floor (`minSignalChars`/`minSignalTokenCount`).
151
+ - `would-rotate`: `dryRun=true` synthetic rotate event (no reset mutation).
149
152
  - `rotated`: actual session rotation happened.
@@ -15,15 +15,54 @@
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
+ }
27
66
  },
28
67
  "dryRun": {
29
68
  "type": "boolean",
@@ -79,10 +118,61 @@
79
118
  "maximum": 8,
80
119
  "default": 1.2
81
120
  },
121
+ "minUniqueTokenRatio": {
122
+ "type": "number",
123
+ "minimum": 0,
124
+ "maximum": 1,
125
+ "default": 0.34
126
+ },
127
+ "shortMessageTokenLimit": {
128
+ "type": "integer",
129
+ "minimum": 1,
130
+ "maximum": 40,
131
+ "default": 6
132
+ },
133
+ "embeddingTriggerMargin": {
134
+ "type": "number",
135
+ "minimum": 0,
136
+ "maximum": 0.5,
137
+ "default": 0.08
138
+ },
82
139
  "stripEnvelope": {
83
140
  "type": "boolean",
84
141
  "default": true
85
142
  },
143
+ "stripRules": {
144
+ "type": "object",
145
+ "additionalProperties": false,
146
+ "properties": {
147
+ "dropLinePrefixPatterns": {
148
+ "type": "array",
149
+ "items": {
150
+ "type": "string"
151
+ },
152
+ "default": []
153
+ },
154
+ "dropExactLines": {
155
+ "type": "array",
156
+ "items": {
157
+ "type": "string"
158
+ },
159
+ "default": []
160
+ },
161
+ "dropFencedBlockAfterHeaderPatterns": {
162
+ "type": "array",
163
+ "items": {
164
+ "type": "string"
165
+ },
166
+ "default": []
167
+ }
168
+ }
169
+ },
170
+ "handoffTailReadMaxBytes": {
171
+ "type": "integer",
172
+ "minimum": 65536,
173
+ "maximum": 8388608,
174
+ "default": 524288
175
+ },
86
176
  "softConsecutiveSignals": {
87
177
  "type": "integer",
88
178
  "minimum": 1,
@@ -137,52 +227,6 @@
137
227
  "type": "string"
138
228
  },
139
229
  "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
230
  }
187
231
  }
188
232
  }
@@ -193,13 +237,13 @@
193
237
  "label": "Behavior Preset",
194
238
  "help": "conservative = fewer resets, balanced = default, aggressive = faster resets."
195
239
  },
196
- "embeddings": {
197
- "label": "Embeddings",
198
- "help": "auto uses whichever embedding backend your instance already has configured."
240
+ "embedding": {
241
+ "label": "Embedding Backend",
242
+ "help": "Set one provider config for embeddings used by this plugin."
199
243
  },
200
244
  "handoff": {
201
245
  "label": "Context Handoff",
202
- "help": "summary keeps transitions transparent without copying full transcript."
246
+ "help": "mode=summary is the safest default; verbatim_last_n copies recent transcript lines."
203
247
  },
204
248
  "dryRun": {
205
249
  "label": "Dry Run",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-topic-shift-reset",
3
- "version": "0.1.0",
3
+ "version": "0.2.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",