@nowledge/openclaw-nowledge-mem 0.6.13 → 0.6.15

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/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  All notable changes to the Nowledge Mem OpenClaw plugin will be documented in this file.
4
4
 
5
+ ## [0.6.15] - 2026-03-18
6
+
7
+ ### Changed
8
+
9
+ - **Shared config for remote credentials.** The plugin now reads `apiUrl` and `apiKey` from `~/.nowledge-mem/config.json` — the same file used by nmem CLI, Bub, Claude Code, and other integrations. One config file connects all your tools. The legacy `~/.nowledge-mem/openclaw.json` is still honored at highest priority for backward compatibility, but is no longer the recommended path. New cascade for credentials: `openclaw.json` (legacy) > OpenClaw dashboard > `config.json` (shared) > env vars > defaults.
10
+
11
+ ### Fixed
12
+
13
+ - **Trailing slash in API URL no longer causes 404.** URLs like `https://mem.example.com/` (with trailing slash) produced double-slash paths in API fallback requests. The URL is now normalized at construction time.
14
+
15
+ ## [0.6.14] - 2026-03-17
16
+
17
+ ### Added
18
+
19
+ - **Configurable thread message truncation.** New `maxThreadMessageChars` setting (200-20000, default 800) controls how many characters are preserved per captured thread message. Higher values keep more context in long conversations. Configurable via dashboard, config file, or `NMEM_MAX_THREAD_MESSAGE_CHARS` env var. Contributed by @blessonism.
20
+
5
21
  ## [0.6.13] - 2026-03-17
6
22
 
7
23
  ### Fixed
package/CLAUDE.md CHANGED
@@ -9,7 +9,7 @@ Continuation guide for `community/nowledge-mem-openclaw-plugin`.
9
9
  - Memory backend: `nmem` CLI (fallback: `uvx --from nmem-cli nmem`)
10
10
  - OpenClaw minimum: `2026.3.7` (`appendSystemContext` / system-context guidance required)
11
11
  - Architecture: **CLI-first via OpenClaw runtime** - all CLI execution goes through `api.runtime.system.runCommandWithTimeout`, not direct `child_process`
12
- - Remote mode: set `NMEM_API_URL` + `NMEM_API_KEY` env vars or config file `apiUrl` + `apiKey`
12
+ - Remote mode: `~/.nowledge-mem/config.json` (shared) or OpenClaw dashboard. Legacy `openclaw.json` still honored.
13
13
 
14
14
  ## Design Philosophy
15
15
 
@@ -30,7 +30,7 @@ src/
30
30
  index.js - plugin registration (tools, hooks, commands, CLI)
31
31
  client.js - CLI wrapper with API fallback; async runtime command execution; credential handling
32
32
  spawn-env.js - env-only credential injection for the nmem runner
33
- config.js - config cascade: ~/.nowledge-mem/openclaw.json > pluginConfig > env vars > defaults
33
+ config.js - config cascade: openclaw.json (legacy) > pluginConfig > config.json (credentials) > env > defaults
34
34
  hooks/
35
35
  behavioral.js - always-on behavioral guidance (~50 tokens/turn)
36
36
  recall.js - before_prompt_build: inject Working Memory + recalled memories
@@ -49,7 +49,8 @@ skills/
49
49
  memory-guide/
50
50
  SKILL.md - agent behavioral skill: when/how to search, save, explore memory (auto-discovered by OpenClaw)
51
51
  openclaw.plugin.json - manifest + config schema (version, uiHints, configSchema, skills)
52
- ~/.nowledge-mem/openclaw.json - optional user config file (overrides OpenClaw settings when present)
52
+ ~/.nowledge-mem/openclaw.json - legacy config file (still honored, deprecated in docs)
53
+ ~/.nowledge-mem/config.json - shared credentials (apiUrl/apiKey) read by all Nowledge Mem tools
53
54
  ```
54
55
 
55
56
  ## Tool Surface (10 tools)
@@ -92,7 +93,8 @@ openclaw.plugin.json - manifest + config schema (version, uiHints, configSchema,
92
93
 
93
94
  ## Config Keys
94
95
 
95
- Optional config file at `~/.nowledge-mem/openclaw.json`. Falls through to OpenClaw plugin settings when the file is absent.
96
+ Plugin settings: OpenClaw dashboard (pluginConfig). Legacy `~/.nowledge-mem/openclaw.json` still honored.
97
+ Credentials (apiUrl/apiKey): also reads `~/.nowledge-mem/config.json` (shared with all Nowledge Mem tools).
96
98
 
97
99
  | Key | Type | Default | Env Var | Description |
98
100
  |-----|------|---------|---------|-------------|
@@ -101,12 +103,14 @@ Optional config file at `~/.nowledge-mem/openclaw.json`. Falls through to OpenCl
101
103
  | `digestMinInterval` | integer 0-86400 | `300` | `NMEM_DIGEST_MIN_INTERVAL` | Minimum seconds between session digests |
102
104
  | `maxContextResults` | integer 1-20 | `5` | `NMEM_MAX_CONTEXT_RESULTS` | How many memories to inject at prompt time |
103
105
  | `recallMinScore` | integer 0-100 | `0` | `NMEM_RECALL_MIN_SCORE` | Min relevance score (%) to include in auto-recall |
106
+ | `maxThreadMessageChars` | integer 200-20000 | `800` | `NMEM_MAX_THREAD_MESSAGE_CHARS` | Max chars per captured thread message before truncation |
104
107
  | `apiUrl` | string | `""` | `NMEM_API_URL` | Remote server URL. Empty = local (127.0.0.1:14242) |
105
108
  | `apiKey` | string | `""` | `NMEM_API_KEY` | API key. Never logged. |
106
109
 
107
- **Priority**: config file > pluginConfig > env var > default.
110
+ **Priority** (plugin-specific keys): `openclaw.json` (legacy) > pluginConfig > env var > default.
111
+ **Priority** (apiUrl/apiKey): `openclaw.json` (legacy) > pluginConfig > `config.json` (shared) > env var > default.
108
112
 
109
- `parseConfig()` returns `_sources` object tracking where each value came from (`"file"`, `"pluginConfig"`, `"env"`, `"default"`). Used by `nowledge_mem_status` tool.
113
+ `parseConfig()` returns `_sources` object tracking where each value came from (`"file"`, `"pluginConfig"`, `"sharedConfig"`, `"env"`, `"default"`). Used by `nowledge_mem_status` tool.
110
114
 
111
115
  Legacy aliases accepted silently in all sources for backward compat:
112
116
  `autoRecall`→`sessionContext`, `autoCapture`→`sessionDigest`, `captureMinInterval`→`digestMinInterval`, `maxRecallResults`→`maxContextResults`.
@@ -156,6 +160,16 @@ nmem wm patch --heading "## Notes" --append "New note from agent"
156
160
  NMEM_API_URL=https://your-server NMEM_API_KEY=key nmem status
157
161
  ```
158
162
 
163
+ ## Version Bump Checklist
164
+
165
+ All three files must match on every release:
166
+
167
+ 1. **`package.json`** — `"version"` (npm registry reads this for `npm publish`)
168
+ 2. **`openclaw.plugin.json`** — `"version"` (OpenClaw runtime reads this at load time)
169
+ 3. **`CHANGELOG.md`** — new `## [x.y.z] - YYYY-MM-DD` section header
170
+
171
+ After bumping, commit inside the `community/` submodule, then stage the updated submodule ref in the parent repo.
172
+
159
173
  ## Known Gaps / Accepted Limitations
160
174
 
161
175
  1. **Feed API `date_from`/`date_to`** - supported. Backend filters events by YYYY-MM-DD range. CLI: `nmem f --from/--to`. Plugin: `nowledge_mem_timeline` accepts `date_from`/`date_to`.
package/README.md CHANGED
@@ -368,26 +368,22 @@ To change settings, use the OpenClaw plugin settings UI. Changes take effect on
368
368
  | `digestMinInterval` | integer | `300` | Minimum seconds between session digests for the same thread (0-86400) |
369
369
  | `maxContextResults` | integer | `5` | Max memories to inject at prompt time (1-20, only used when sessionContext is enabled) |
370
370
  | `recallMinScore` | integer | `0` | Min relevance score (0-100%) to include in auto-recall. 0 = include all |
371
+ | `maxThreadMessageChars` | integer | `800` | Maximum characters preserved per captured OpenClaw thread message before truncation |
371
372
  | `apiUrl` | string | `""` | Remote server URL. Empty = local (`http://127.0.0.1:14242`) |
372
373
  | `apiKey` | string | `""` | API key for remote access. Injected as `NMEM_API_KEY` env var, never logged |
373
374
 
374
- ### Advanced: config file
375
+ ### Remote access
375
376
 
376
- For persistent or scripted config, create `~/.nowledge-mem/openclaw.json`:
377
+ Create `~/.nowledge-mem/config.json` with your credentials. This file is shared by all Nowledge Mem integrations (nmem CLI, Bub, Claude Code, etc.) so one file connects everything:
377
378
 
378
379
  ```json
379
380
  {
380
- "sessionContext": false,
381
- "sessionDigest": true,
382
- "digestMinInterval": 300,
383
- "maxContextResults": 5,
384
- "recallMinScore": 0,
385
- "apiUrl": "",
386
- "apiKey": ""
381
+ "apiUrl": "https://<your-url>",
382
+ "apiKey": "nmem_..."
387
383
  }
388
384
  ```
389
385
 
390
- The config file takes priority over OpenClaw settings. Only the keys you include are overridden; missing keys fall through to OpenClaw settings, then env vars, then defaults.
386
+ See [Access Mem Anywhere](https://mem.nowledge.co/docs/remote-access).
391
387
 
392
388
  ### Advanced: environment variables
393
389
 
@@ -399,16 +395,25 @@ NMEM_SESSION_DIGEST=true
399
395
  NMEM_DIGEST_MIN_INTERVAL=300
400
396
  NMEM_MAX_CONTEXT_RESULTS=5
401
397
  NMEM_RECALL_MIN_SCORE=0
398
+ NMEM_MAX_THREAD_MESSAGE_CHARS=800
402
399
  NMEM_API_URL=https://...
403
400
  NMEM_API_KEY=your-key
404
401
  ```
405
402
 
406
403
  ### Priority
407
404
 
405
+ Plugin-specific settings:
408
406
  ```
409
- config file > OpenClaw settings > env vars > defaults
407
+ openclaw.json (legacy) > OpenClaw dashboard > env vars > defaults
410
408
  ```
411
409
 
410
+ Credentials (apiUrl, apiKey):
411
+ ```
412
+ openclaw.json (legacy) > OpenClaw dashboard > config.json (shared) > env vars > defaults
413
+ ```
414
+
415
+ `~/.nowledge-mem/openclaw.json` is still honored for backward compatibility but is no longer the recommended path. New users should configure plugin-specific settings via the OpenClaw dashboard and shared credentials via `~/.nowledge-mem/config.json`.
416
+
412
417
  Use `nowledge_mem_status` (or `openclaw nowledge-mem status`) to see where each value comes from.
413
418
 
414
419
  ### Trust warning after install
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "openclaw-nowledge-mem",
3
- "version": "0.6.13",
3
+ "version": "0.6.15",
4
4
  "kind": "memory",
5
5
  "skills": ["skills/memory-guide"],
6
6
  "uiHints": {
@@ -24,6 +24,10 @@
24
24
  "label": "Min recall score (%)",
25
25
  "help": "Only inject memories with relevance score above this threshold (0-100). Set to 0 to include all results."
26
26
  },
27
+ "maxThreadMessageChars": {
28
+ "label": "Max thread message chars",
29
+ "help": "Maximum characters preserved per captured OpenClaw thread message (200-20000). Higher values keep more of long messages in Nowledge thread history."
30
+ },
27
31
  "apiUrl": {
28
32
  "label": "Server URL (remote mode)",
29
33
  "help": "Leave empty for local mode (default: http://127.0.0.1:14242). Set to your remote server URL for cross-device or team access. See: https://docs.nowledge.co/docs/remote-access"
@@ -68,6 +72,13 @@
68
72
  "maximum": 100,
69
73
  "description": "Minimum relevance score (0-100%) to include a memory in auto-recall. Set to 0 to include all results."
70
74
  },
75
+ "maxThreadMessageChars": {
76
+ "type": "integer",
77
+ "default": 800,
78
+ "minimum": 200,
79
+ "maximum": 20000,
80
+ "description": "Maximum characters preserved per captured OpenClaw thread message before truncation."
81
+ },
71
82
  "apiUrl": {
72
83
  "type": "string",
73
84
  "default": "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowledge/openclaw-nowledge-mem",
3
- "version": "0.6.13",
3
+ "version": "0.6.15",
4
4
  "type": "module",
5
5
  "description": "Nowledge Mem memory plugin for OpenClaw, local-first personal knowledge base",
6
6
  "author": {
package/src/client.js CHANGED
@@ -83,7 +83,7 @@ export class NowledgeMemClient {
83
83
  this.nmemCmdPromise = null;
84
84
  // Resolved once from config + env (config wins over env, both win over default)
85
85
  this._apiUrl =
86
- (credentials.apiUrl || "").trim() || "http://127.0.0.1:14242";
86
+ ((credentials.apiUrl || "").trim() || "http://127.0.0.1:14242").replace(/\/+$/, "");
87
87
  this._apiKey = (credentials.apiKey || "").trim();
88
88
  }
89
89
 
package/src/config.js CHANGED
@@ -20,6 +20,7 @@ const ALLOWED_KEYS = new Set([
20
20
  "digestMinInterval",
21
21
  "maxContextResults",
22
22
  "recallMinScore",
23
+ "maxThreadMessageChars",
23
24
  "apiUrl",
24
25
  "apiKey",
25
26
  // Legacy aliases — accepted but not advertised
@@ -38,13 +39,20 @@ const ALIAS_KEYS = {
38
39
  maxRecallResults: "maxContextResults",
39
40
  };
40
41
 
41
- // --- config file ---------------------------------------------------------
42
+ // --- config files --------------------------------------------------------
42
43
 
43
44
  const CONFIG_DIR = join(homedir(), ".nowledge-mem");
45
+
46
+ // Legacy dedicated config (deprecated — still honored for backward compat)
44
47
  const CONFIG_PATH = join(CONFIG_DIR, "openclaw.json");
45
48
 
49
+ // Shared config read by all Nowledge Mem integrations (nmem CLI, Bub, Claude Code, etc.)
50
+ const SHARED_CONFIG_PATH = join(CONFIG_DIR, "config.json");
51
+
46
52
  /**
47
53
  * Read ~/.nowledge-mem/openclaw.json if it exists.
54
+ * Deprecated: new users should use the OpenClaw dashboard for plugin settings
55
+ * and ~/.nowledge-mem/config.json for shared credentials (apiUrl, apiKey).
48
56
  * Returns {} when the file is missing or unreadable.
49
57
  */
50
58
  function readConfigFile(logger) {
@@ -63,8 +71,37 @@ function readConfigFile(logger) {
63
71
  }
64
72
  }
65
73
 
74
+ /**
75
+ * Read apiUrl and apiKey from the shared ~/.nowledge-mem/config.json.
76
+ * Only credentials are extracted — plugin-specific keys are ignored.
77
+ * Returns { apiUrl?, apiKey? } or {} when the file is missing or unreadable.
78
+ */
79
+ function readSharedConfig(logger) {
80
+ try {
81
+ if (!existsSync(SHARED_CONFIG_PATH)) {
82
+ return {};
83
+ }
84
+ const content = readFileSync(SHARED_CONFIG_PATH, "utf-8").trim();
85
+ if (!content) return {};
86
+ const parsed = JSON.parse(content);
87
+ const result = {};
88
+ if (typeof parsed.apiUrl === "string" && parsed.apiUrl.trim()) {
89
+ result.apiUrl = parsed.apiUrl.trim();
90
+ }
91
+ if (typeof parsed.apiKey === "string" && parsed.apiKey.trim()) {
92
+ result.apiKey = parsed.apiKey.trim();
93
+ }
94
+ return result;
95
+ } catch (err) {
96
+ logger?.warn?.(
97
+ `nowledge-mem: could not read ${SHARED_CONFIG_PATH}: ${err instanceof Error ? err.message : err}`,
98
+ );
99
+ return {};
100
+ }
101
+ }
102
+
66
103
  /** Exported for tests / CLI tooling. */
67
- export { CONFIG_PATH };
104
+ export { CONFIG_PATH, SHARED_CONFIG_PATH };
68
105
 
69
106
  // --- env var helpers -----------------------------------------------------
70
107
 
@@ -129,14 +166,21 @@ function firstDefined(...options) {
129
166
 
130
167
  /**
131
168
  * Parse plugin config with cascade:
132
- * ~/.nowledge-mem/openclaw.json > pluginConfig > env vars > defaults
133
169
  *
134
- * The config file (if it exists) takes highest priority so users who
135
- * create it get predictable behavior. Most users won't have one and
136
- * will configure via OpenClaw's plugin settings UI (pluginConfig).
170
+ * Plugin-specific keys (sessionContext, sessionDigest, etc.):
171
+ * openclaw.json (legacy) > pluginConfig (dashboard) > env vars > defaults
172
+ *
173
+ * Credentials (apiUrl, apiKey):
174
+ * openclaw.json (legacy) > pluginConfig (dashboard) > config.json (shared) > env vars > defaults
175
+ *
176
+ * Most users configure via the OpenClaw dashboard (pluginConfig) and
177
+ * ~/.nowledge-mem/config.json for shared credentials across all tools.
178
+ *
179
+ * ~/.nowledge-mem/openclaw.json is still honored for backward compat
180
+ * but is no longer the recommended path.
137
181
  *
138
182
  * Canonical keys: sessionContext, sessionDigest, digestMinInterval,
139
- * maxContextResults, recallMinScore, apiUrl, apiKey
183
+ * maxContextResults, recallMinScore, maxThreadMessageChars, apiUrl, apiKey
140
184
  *
141
185
  * Legacy aliases (accepted from all sources; never shown in docs):
142
186
  * autoRecall → sessionContext
@@ -150,12 +194,14 @@ function firstDefined(...options) {
150
194
  * NMEM_DIGEST_MIN_INTERVAL — seconds (0-86400)
151
195
  * NMEM_MAX_CONTEXT_RESULTS — integer (1-20)
152
196
  * NMEM_RECALL_MIN_SCORE — integer (0-100)
197
+ * NMEM_MAX_THREAD_MESSAGE_CHARS — integer (200-20000)
153
198
  * NMEM_API_URL — remote server URL
154
199
  * NMEM_API_KEY — API key (never logged)
155
200
  */
156
201
  export function parseConfig(raw, logger) {
157
202
  const pluginCfg = raw && typeof raw === "object" ? raw : {};
158
203
  const fileCfg = readConfigFile(logger);
204
+ const sharedCfg = readSharedConfig(logger);
159
205
 
160
206
  // Resolve aliases in both sources
161
207
  const resolvedPlugin = resolveAliases(pluginCfg);
@@ -250,7 +296,24 @@ export function parseConfig(raw, logger) {
250
296
  const recallMinScore = Math.min(100, Math.max(0, Math.trunc(rms.value)));
251
297
  _sources.recallMinScore = rms.source;
252
298
 
253
- // --- apiUrl: file > pluginConfig > env > "" ---
299
+ // --- maxThreadMessageChars: file > pluginConfig > env > default ---
300
+ const mtmcEnv = envInt("NMEM_MAX_THREAD_MESSAGE_CHARS");
301
+ const mtmc = firstDefined(
302
+ { value: pickNum(resolvedFile, "maxThreadMessageChars"), source: "file" },
303
+ {
304
+ value: pickNum(resolvedPlugin, "maxThreadMessageChars"),
305
+ source: "pluginConfig",
306
+ },
307
+ { value: mtmcEnv, source: "env" },
308
+ { value: 800, source: "default" },
309
+ );
310
+ const maxThreadMessageChars = Math.min(
311
+ 20000,
312
+ Math.max(200, Math.trunc(mtmc.value)),
313
+ );
314
+ _sources.maxThreadMessageChars = mtmc.source;
315
+
316
+ // --- apiUrl: file (legacy) > pluginConfig > sharedConfig > env > "" ---
254
317
  const fileUrl =
255
318
  typeof resolvedFile.apiUrl === "string" && resolvedFile.apiUrl.trim();
256
319
  const pluginUrl =
@@ -258,13 +321,14 @@ export function parseConfig(raw, logger) {
258
321
  const au = firstDefined(
259
322
  { value: fileUrl || undefined, source: "file" },
260
323
  { value: pluginUrl || undefined, source: "pluginConfig" },
324
+ { value: sharedCfg.apiUrl || undefined, source: "sharedConfig" },
261
325
  { value: envStr("NMEM_API_URL"), source: "env" },
262
326
  { value: "", source: "default" },
263
327
  );
264
328
  const apiUrl = au.value;
265
329
  _sources.apiUrl = au.source;
266
330
 
267
- // --- apiKey: file > pluginConfig > env > "" ---
331
+ // --- apiKey: file (legacy) > pluginConfig > sharedConfig > env > "" ---
268
332
  const fileKey =
269
333
  typeof resolvedFile.apiKey === "string" && resolvedFile.apiKey.trim();
270
334
  const pluginKey =
@@ -272,6 +336,7 @@ export function parseConfig(raw, logger) {
272
336
  const ak = firstDefined(
273
337
  { value: fileKey || undefined, source: "file" },
274
338
  { value: pluginKey || undefined, source: "pluginConfig" },
339
+ { value: sharedCfg.apiKey || undefined, source: "sharedConfig" },
275
340
  { value: envStr("NMEM_API_KEY"), source: "env" },
276
341
  { value: "", source: "default" },
277
342
  );
@@ -284,6 +349,7 @@ export function parseConfig(raw, logger) {
284
349
  digestMinInterval,
285
350
  maxContextResults,
286
351
  recallMinScore,
352
+ maxThreadMessageChars,
287
353
  apiUrl,
288
354
  apiKey,
289
355
  _sources,
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { readFile } from "node:fs/promises";
3
3
 
4
- const MAX_MESSAGE_CHARS = 800;
4
+ const DEFAULT_MAX_MESSAGE_CHARS = 800;
5
5
  const MAX_DISTILL_MESSAGE_CHARS = 2000;
6
6
  const MAX_CONVERSATION_CHARS = 30_000;
7
7
  const MIN_MESSAGES_FOR_DISTILL = 4;
@@ -23,7 +23,7 @@ function _setLastCapture(threadId, now) {
23
23
  }
24
24
  }
25
25
 
26
- function truncate(text, max = MAX_MESSAGE_CHARS) {
26
+ function truncate(text, max = DEFAULT_MAX_MESSAGE_CHARS) {
27
27
  const str = String(text || "").trim();
28
28
  if (!str) return "";
29
29
  return str.length > max ? `${str.slice(0, max)}…` : str;
@@ -48,7 +48,7 @@ function extractText(content) {
48
48
  return parts.join("\n").trim();
49
49
  }
50
50
 
51
- function normalizeRoleMessage(raw) {
51
+ function normalizeRoleMessage(raw, maxMessageChars = DEFAULT_MAX_MESSAGE_CHARS) {
52
52
  if (!raw || typeof raw !== "object") return null;
53
53
  const msg =
54
54
  raw.message && typeof raw.message === "object" ? raw.message : raw;
@@ -80,7 +80,7 @@ function normalizeRoleMessage(raw) {
80
80
 
81
81
  return {
82
82
  role,
83
- content: truncate(text),
83
+ content: truncate(text, maxMessageChars),
84
84
  fullContent: text,
85
85
  timestamp,
86
86
  externalHint,
@@ -170,7 +170,7 @@ async function resolveHookMessages(event) {
170
170
  return loadMessagesFromSessionFile(sessionFile);
171
171
  }
172
172
 
173
- async function appendOrCreateThread({ client, logger, event, ctx, reason }) {
173
+ async function appendOrCreateThread({ client, logger, event, ctx, reason, maxMessageChars = DEFAULT_MAX_MESSAGE_CHARS }) {
174
174
  const rawMessages = await resolveHookMessages(event);
175
175
  if (!Array.isArray(rawMessages) || rawMessages.length === 0) return;
176
176
 
@@ -178,7 +178,9 @@ async function appendOrCreateThread({ client, logger, event, ctx, reason }) {
178
178
  const sessionKey = String(ctx?.sessionKey || ctx?.sessionId || "session");
179
179
  const sessionId = String(ctx?.sessionId || "").trim();
180
180
  const title = buildThreadTitle(ctx, reason);
181
- const normalized = rawMessages.map(normalizeRoleMessage).filter(Boolean);
181
+ const normalized = rawMessages
182
+ .map((message) => normalizeRoleMessage(message, maxMessageChars))
183
+ .filter(Boolean);
182
184
  if (normalized.length === 0) return;
183
185
 
184
186
  const messages = normalized.map((message, index) => ({
@@ -291,6 +293,7 @@ export function buildAgentEndCaptureHandler(client, cfg, logger) {
291
293
  event,
292
294
  ctx,
293
295
  reason: "agent_end",
296
+ maxMessageChars: cfg.maxThreadMessageChars,
294
297
  });
295
298
 
296
299
  // 2. Triage + distill: language-agnostic LLM-based capture.
@@ -376,6 +379,13 @@ export function buildAgentEndCaptureHandler(client, cfg, logger) {
376
379
  export function buildBeforeResetCaptureHandler(client, _cfg, logger) {
377
380
  return async (event, ctx) => {
378
381
  const reason = typeof event?.reason === "string" ? event.reason : undefined;
379
- await appendOrCreateThread({ client, logger, event, ctx, reason });
382
+ await appendOrCreateThread({
383
+ client,
384
+ logger,
385
+ event,
386
+ ctx,
387
+ reason,
388
+ maxMessageChars: _cfg?.maxThreadMessageChars,
389
+ });
380
390
  };
381
391
  }
@@ -109,6 +109,9 @@ export function createStatusTool(client, _logger, cfg) {
109
109
  lines.push(
110
110
  ` recallMinScore: ${cfg.recallMinScore}% (${sources.recallMinScore || "?"})`,
111
111
  );
112
+ lines.push(
113
+ ` maxThreadMessageChars: ${cfg.maxThreadMessageChars} (${sources.maxThreadMessageChars || "?"})`,
114
+ );
112
115
 
113
116
  details.config = {
114
117
  sessionContext: {
@@ -131,6 +134,10 @@ export function createStatusTool(client, _logger, cfg) {
131
134
  value: cfg.recallMinScore,
132
135
  source: sources.recallMinScore,
133
136
  },
137
+ maxThreadMessageChars: {
138
+ value: cfg.maxThreadMessageChars,
139
+ source: sources.maxThreadMessageChars,
140
+ },
134
141
  apiUrl: { value: cfg.apiUrl || "(local)", source: sources.apiUrl },
135
142
  apiKey: {
136
143
  value: cfg.apiKey ? "(set)" : "(not set)",