openclaw-topic-shift-reset 0.4.1 → 0.4.3

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
@@ -154,8 +154,8 @@ All plugin logs are prefixed with `topic-shift-reset:`.
154
154
 
155
155
  ### Debug (`debug: true`)
156
156
 
157
- - `classify source=<...> kind=<warmup|stable|suspect|rotate-hard|rotate-soft> reason=<...> ...`
158
- Full classifier output and metrics for a processed message.
157
+ - `classify source=<...> kind=<warmup|stable|suspect|rotate-hard|rotate-soft> reason=<...> ... textHash=<...> tokens=[...] text="..."`
158
+ Full classifier output and metrics plus a compact message preview for a processed message.
159
159
  - `suspect-queued session=<...>`
160
160
  Soft-suspect state queued for clarification steering.
161
161
  - `ask-injected session=<...>`
@@ -47,7 +47,7 @@ With `softSuspect.mode: "strict"` (default), a soft-confirm reset is blocked unt
47
47
  - `softSuspect.prompt`: optional steer text injected on soft-suspect.
48
48
  - `softSuspect.ttlSeconds`: expiry for pending soft-suspect steer.
49
49
  - `dryRun`: logs would-rotate events without session resets.
50
- - `debug`: emits per-message classifier diagnostics.
50
+ - `debug`: emits per-message classifier diagnostics, including compact message previews.
51
51
 
52
52
  ## Built-in preset defaults
53
53
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-topic-shift-reset",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
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",
package/src/index.ts CHANGED
@@ -329,6 +329,8 @@ const STALE_SESSION_STATE_MS = 4 * 60 * 60 * 1000;
329
329
  const MAX_RECENT_MESSAGE_EVENTS = 20_000;
330
330
  const MESSAGE_EVENT_TTL_MS = 5 * 60 * 1000;
331
331
  const ROTATION_DEDUPE_MS = 25_000;
332
+ const DEBUG_LOG_TEXT_PREVIEW_CHARS = 180;
333
+ const DEBUG_LOG_TOKEN_PREVIEW_COUNT = 12;
332
334
  const PERSISTENCE_SCHEMA_VERSION = 1;
333
335
  const PERSISTENCE_FILE_NAME = "runtime-state.v1.json";
334
336
  const PERSISTENCE_FLUSH_DEBOUNCE_MS = 1_200;
@@ -1250,8 +1252,9 @@ function classifyMessage(params: {
1250
1252
  now: number;
1251
1253
  }): ClassificationDecision {
1252
1254
  const { cfg, state, now } = params;
1255
+ const hasSimilarity = params.usedEmbedding && typeof params.similarity === "number";
1253
1256
  const score =
1254
- params.usedEmbedding && typeof params.similarity === "number"
1257
+ hasSimilarity
1255
1258
  ? 0.7 * (1 - params.similarity) +
1256
1259
  0.15 * params.lexical.lexicalDistance +
1257
1260
  0.15 * params.lexical.novelty
@@ -1279,21 +1282,25 @@ function classifyMessage(params: {
1279
1282
  return { kind: "stable", metrics, reason: "cooldown" };
1280
1283
  }
1281
1284
 
1285
+ const lexicalHardSignal =
1286
+ params.lexical.score >= cfg.hardScoreThreshold ||
1287
+ (params.lexical.novelty >= cfg.hardNoveltyThreshold &&
1288
+ params.lexical.lexicalDistance >= 0.65);
1282
1289
  const hardSignal =
1283
- score >= cfg.hardScoreThreshold ||
1284
- (params.usedEmbedding && typeof params.similarity === "number"
1285
- ? params.similarity <= cfg.hardSimilarityThreshold &&
1286
- params.lexical.novelty >= cfg.hardNoveltyThreshold
1287
- : params.lexical.novelty >= cfg.hardNoveltyThreshold &&
1288
- params.lexical.lexicalDistance >= 0.65);
1290
+ hasSimilarity &&
1291
+ (score >= cfg.hardScoreThreshold ||
1292
+ (params.similarity <= cfg.hardSimilarityThreshold &&
1293
+ params.lexical.novelty >= cfg.hardNoveltyThreshold));
1289
1294
 
1290
1295
  if (hardSignal) {
1291
1296
  return { kind: "rotate-hard", metrics, reason: "hard-threshold" };
1292
1297
  }
1293
1298
 
1299
+ const forceSoftPathFromLexicalHard = !hasSimilarity && lexicalHardSignal;
1294
1300
  const softSignal =
1301
+ forceSoftPathFromLexicalHard ||
1295
1302
  score >= cfg.softScoreThreshold ||
1296
- (params.usedEmbedding && typeof params.similarity === "number"
1303
+ (hasSimilarity
1297
1304
  ? params.similarity <= cfg.softSimilarityThreshold &&
1298
1305
  params.lexical.novelty >= cfg.softNoveltyThreshold
1299
1306
  : params.lexical.novelty >= cfg.softNoveltyThreshold &&
@@ -2057,23 +2064,31 @@ export default function register(api: OpenClawPluginApi): void {
2057
2064
  }
2058
2065
 
2059
2066
  if (cfg.debug) {
2060
- api.logger.debug(
2061
- [
2062
- `topic-shift-reset: classify`,
2063
- `source=${params.source}`,
2064
- `kind=${decision.kind}`,
2065
- `reason=${decision.reason}`,
2066
- `session=${sessionKey}`,
2067
- `score=${decision.metrics.score.toFixed(3)}`,
2068
- `novelty=${decision.metrics.novelty.toFixed(3)}`,
2069
- `lex=${decision.metrics.lexicalDistance.toFixed(3)}`,
2070
- `uniq=${decision.metrics.uniqueTokenRatio.toFixed(3)}`,
2071
- `entropy=${decision.metrics.entropy.toFixed(3)}`,
2072
- `sim=${typeof decision.metrics.similarity === "number" ? decision.metrics.similarity.toFixed(3) : "n/a"}`,
2073
- `embed=${decision.metrics.usedEmbedding ? "1" : "0"}`,
2074
- `pending=${state.pendingSoftSignals}`,
2075
- ].join(" "),
2076
- );
2067
+ const textPreview = truncateText(text, DEBUG_LOG_TEXT_PREVIEW_CHARS);
2068
+ const rawPreview = truncateText(rawText, DEBUG_LOG_TEXT_PREVIEW_CHARS);
2069
+ const debugFields = [
2070
+ `topic-shift-reset: classify`,
2071
+ `source=${params.source}`,
2072
+ `kind=${decision.kind}`,
2073
+ `reason=${decision.reason}`,
2074
+ `session=${sessionKey}`,
2075
+ `score=${decision.metrics.score.toFixed(3)}`,
2076
+ `novelty=${decision.metrics.novelty.toFixed(3)}`,
2077
+ `lex=${decision.metrics.lexicalDistance.toFixed(3)}`,
2078
+ `uniq=${decision.metrics.uniqueTokenRatio.toFixed(3)}`,
2079
+ `entropy=${decision.metrics.entropy.toFixed(3)}`,
2080
+ `sim=${typeof decision.metrics.similarity === "number" ? decision.metrics.similarity.toFixed(3) : "n/a"}`,
2081
+ `embed=${decision.metrics.usedEmbedding ? "1" : "0"}`,
2082
+ `pending=${state.pendingSoftSignals}`,
2083
+ `baseline=${baselineTokens.size}`,
2084
+ `textHash=${contentHash}`,
2085
+ `tokens=${maybeJson(tokenList.slice(0, DEBUG_LOG_TOKEN_PREVIEW_COUNT))}`,
2086
+ `text=${maybeJson(textPreview)}`,
2087
+ ];
2088
+ if (rawPreview !== textPreview) {
2089
+ debugFields.push(`raw=${maybeJson(rawPreview)}`);
2090
+ }
2091
+ api.logger.debug(debugFields.join(" "));
2077
2092
  }
2078
2093
 
2079
2094
  if (decision.kind === "warmup") {