@nowledge/openclaw-nowledge-mem 0.8.27 → 0.8.31

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
@@ -4,6 +4,32 @@ All notable changes to the Nowledge Mem OpenClaw plugin will be documented in th
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## [0.8.31] - 2026-07-12
8
+
9
+ ### Improved
10
+
11
+ - New OpenClaw Threads use the first meaningful user message as their title instead of exposing an internal `agent:...` session key. When no user turn is available, the plugin falls back to a readable channel label such as `OpenClaw · WeChat`.
12
+
13
+ ## [0.8.30] - 2026-07-12
14
+
15
+ ### Fixed
16
+
17
+ - Setup guidance now preserves existing OpenClaw tool policy while granting the complete plugin contract under restrictive profiles such as `coding`. It uses the plugin id in `tools.alsoAllow` (or an existing `tools.allow`) instead of fragile individual tool names.
18
+ - Setup now explicitly grants `plugins.entries.openclaw-nowledge-mem.hooks.allowConversationAccess=true`, which current OpenClaw releases require before non-bundled plugins can use `agent_end` as the thread-capture safety net.
19
+ - `nowledge_mem_status` now reports restricted tool profiles and blocked conversation hooks instead of claiming all tools and both capture paths are active.
20
+
21
+ ## [0.8.29] - 2026-07-11
22
+
23
+ ### Fixed
24
+
25
+ - Conversation capture now converts OpenClaw's numeric message timestamps to the textual timestamp format required by Mem. Automatic thread sync no longer stops while memory tools continue to work.
26
+
27
+ ## [0.8.28] - 2026-07-08
28
+
29
+ ### Improved
30
+
31
+ - `nowledge_mem_status` now checks OpenClaw's Mem HTTP thread-sync API separately from the `nmem` CLI. This makes session-capture issues visible when memory tools still work but conversation sync cannot reach `/threads`.
32
+
7
33
  ## [0.8.27] - 2026-06-26
8
34
 
9
35
  ### Fixed
package/README.md CHANGED
@@ -17,7 +17,15 @@ This is not a flat note store. Nowledge Mem links related knowledge into a graph
17
17
  openclaw plugins install clawhub:@nowledge/openclaw-nowledge-mem
18
18
  ```
19
19
 
20
- OpenClaw's installer writes the install record, enables the plugin, and switches the `memory` slot to `openclaw-nowledge-mem`. On current OpenClaw builds, that same install flow may also set `plugins.slots.contextEngine` to `openclaw-nowledge-mem`. Plugin `0.8.18+` accepts that automatically as a compatibility alias, so you do not need to hand-edit the file after install. If you manage config manually, keep using the canonical context engine id `nowledge-mem`.
20
+ OpenClaw's installer writes the install record, enables the plugin, and switches the `memory` slot to `openclaw-nowledge-mem`. On current OpenClaw builds, that same install flow may also set `plugins.slots.contextEngine` to `openclaw-nowledge-mem`. Plugin `0.8.18+` accepts that automatically as a compatibility alias.
21
+
22
+ Grant the conversation hook used as the thread-sync safety net:
23
+
24
+ ```bash
25
+ openclaw config set plugins.entries.openclaw-nowledge-mem.hooks.allowConversationAccess true --strict-json
26
+ ```
27
+
28
+ If `openclaw config get tools` shows a restrictive profile such as `coding`, preserve its existing policy and add `openclaw-nowledge-mem` to `tools.alsoAllow`. If `tools.allow` already exists, add the plugin id there instead. The plugin id expands to all manifest-declared tools; do not list individual `nowledge_mem_*` names.
21
29
 
22
30
  To update the plugin, re-run the ClawHub install with `--force`:
23
31
 
@@ -434,7 +442,7 @@ openclaw nowledge-mem status
434
442
 
435
443
  ## Configuration
436
444
 
437
- No config is required for a normal npm install. The defaults work for local mode, and `openclaw plugins install` already enables the plugin and selects the memory slot.
445
+ The plugin defaults work for local Mem, and `openclaw plugins install` enables the plugin and selects the memory slot. Current OpenClaw releases still require one explicit conversation-hook grant for the `agent_end` capture fallback. Restrictive tool profiles also need the plugin id added to their effective allow policy; see Installation above.
438
446
 
439
447
  To change settings, use the OpenClaw plugin settings UI. Changes take effect on restart.
440
448
 
package/SKILL.md CHANGED
@@ -129,6 +129,37 @@ Important facts:
129
129
 
130
130
  Do not tell the user to hand-edit those settings unless they are using a manual/path setup.
131
131
 
132
+ ### Step 3a — Grant the Runtime Capabilities the Plugin Uses
133
+
134
+ Current OpenClaw releases require an explicit grant before a non-bundled plugin
135
+ can read conversation content from `agent_end`. Set it after install:
136
+
137
+ ```bash
138
+ openclaw config set plugins.entries.openclaw-nowledge-mem.hooks.allowConversationAccess true --strict-json
139
+ ```
140
+
141
+ Then inspect the host's effective tool policy:
142
+
143
+ ```bash
144
+ openclaw config get tools
145
+ ```
146
+
147
+ - If `tools.allow` already exists, preserve that list and add
148
+ `openclaw-nowledge-mem` to it.
149
+ - Otherwise, when a restrictive profile such as `coding`, `messaging`, or
150
+ `minimal` is set, preserve `tools.alsoAllow` and add
151
+ `openclaw-nowledge-mem` to it.
152
+ - If no restrictive profile or allowlist exists, no tool-policy change is
153
+ needed.
154
+ - Never replace an existing list, and never add individual `nowledge_mem_*`
155
+ names. The plugin id expands to the complete manifest-declared tool contract.
156
+
157
+ Example for a `coding` profile with no prior `alsoAllow` entries:
158
+
159
+ ```bash
160
+ openclaw config set tools.alsoAllow '["openclaw-nowledge-mem"]' --strict-json
161
+ ```
162
+
132
163
  ### Step 4 — Configure Remote Only When Needed
133
164
 
134
165
  Local mode:
@@ -189,7 +220,7 @@ First check the plugin status:
189
220
  openclaw nowledge-mem status
190
221
  ```
191
222
 
192
- If only `memory_search` and `memory_get` tools are available (other Nowledge Mem tools missing), the memory slot may still point to the built-in `memory-core`. Verify the slot in `~/.openclaw/openclaw.json`:
223
+ If only `memory_search` and `memory_get` tools are available (other Nowledge Mem tools missing), inspect `openclaw config get tools` first. A restrictive profile needs the plugin id in `tools.alsoAllow`, or in the existing `tools.allow` list. Then verify the memory slot in `~/.openclaw/openclaw.json`:
193
224
 
194
225
  ```json
195
226
  {
@@ -253,7 +284,8 @@ What to expect:
253
284
  | `plugins.allow is empty` warning | Add `openclaw-nowledge-mem` to `plugins.allow` if the user wants explicit trust |
254
285
  | Remote config seems ignored | Check whether `~/.nowledge-mem/openclaw.json` is overriding plugin settings |
255
286
  | Local mode unexpectedly talks to a remote server | Check for stale `NMEM_API_URL` / `NMEM_API_KEY` in the environment or an overriding `~/.nowledge-mem/openclaw.json` |
256
- | Plugin tools missing | Ensure the plugin is in `plugins.allow: ["openclaw-nowledge-mem"]`. Do **not** put `nowledge_mem_*` tool names in `tools.allow` — OpenClaw silently strips plugin-only allowlists. No `tools.*` config is needed; plugin tools load automatically when the plugin is allowed. |
287
+ | Plugin tools missing | Inspect `openclaw config get tools`. Under a restrictive profile, preserve `tools.alsoAllow` and add `openclaw-nowledge-mem`; if `tools.allow` already exists, add the plugin id there instead. Do not add individual `nowledge_mem_*` names. Also ensure the plugin is trusted in `plugins.allow`. |
288
+ | Memory tools work but Threads do not sync | Update to `0.8.30+`, set `plugins.entries.openclaw-nowledge-mem.hooks.allowConversationAccess=true`, restart the gateway, and verify with `nmem t list --source openclaw -n 20`. |
257
289
  | `must declare contracts.tools before registering agent tools` | Update the plugin to `0.8.27+` and restart OpenClaw. Newer OpenClaw builds require plugin authors to declare tool ownership in the manifest before runtime tool registration. |
258
290
 
259
291
  ## Notes for Agents
@@ -2,7 +2,7 @@
2
2
  "id": "openclaw-nowledge-mem",
3
3
  "name": "Nowledge Mem",
4
4
  "description": "Cross-tool knowledge graph memory for OpenClaw with startup context, graph search, and session distillation.",
5
- "version": "0.8.27",
5
+ "version": "0.8.31",
6
6
  "kind": ["memory", "context-engine"],
7
7
  "contracts": {
8
8
  "tools": [
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nowledge/openclaw-nowledge-mem",
3
3
  "displayName": "Nowledge Mem for OpenClaw",
4
- "version": "0.8.27",
4
+ "version": "0.8.31",
5
5
  "type": "module",
6
6
  "description": "Nowledge Mem memory plugin for OpenClaw, local-first personal knowledge base",
7
7
  "author": {
@@ -12,6 +12,7 @@ export const DEFAULT_MAX_MESSAGE_CHARS = 800;
12
12
  export const MAX_DISTILL_MESSAGE_CHARS = 2000;
13
13
  export const MAX_CONVERSATION_CHARS = 30_000;
14
14
  export const MIN_MESSAGES_FOR_DISTILL = 4;
15
+ export const MAX_THREAD_TITLE_CHARS = 80;
15
16
  const SESSION_RESET_PROMPT_PREFIX =
16
17
  "A new session was started via /new or /reset.";
17
18
  const OPENCLAW_DIRECTIVE_TAG_RE =
@@ -246,8 +247,15 @@ export function normalizeRoleMessage(
246
247
  if (role === "user" && text.startsWith("/")) return null;
247
248
 
248
249
  let timestamp;
249
- if (typeof msg.timestamp === "string" || typeof msg.timestamp === "number") {
250
+ if (typeof msg.timestamp === "string") {
250
251
  timestamp = msg.timestamp;
252
+ } else if (
253
+ typeof msg.timestamp === "number" &&
254
+ Number.isFinite(msg.timestamp)
255
+ ) {
256
+ // OpenClaw emits epoch milliseconds, while Mem's thread API deliberately
257
+ // accepts textual timestamps so every connector shares one wire contract.
258
+ timestamp = new Date(msg.timestamp).toISOString();
251
259
  }
252
260
 
253
261
  const openclawMeta =
@@ -302,9 +310,47 @@ export function normalizeRoleMessage(
302
310
  };
303
311
  }
304
312
 
305
- export function buildThreadTitle(ctx) {
306
- const session = ctx?.sessionKey || ctx?.sessionId || "session";
307
- return `OpenClaw ${session}`;
313
+ function humanizeChannel(value) {
314
+ const normalized = String(value || "")
315
+ .trim()
316
+ .toLowerCase();
317
+ if (!normalized) return "";
318
+ const known = {
319
+ dashboard: "Dashboard",
320
+ "openclaw-weixin": "WeChat",
321
+ weixin: "WeChat",
322
+ wechat: "WeChat",
323
+ telegram: "Telegram",
324
+ discord: "Discord",
325
+ whatsapp: "WhatsApp",
326
+ };
327
+ if (known[normalized]) return known[normalized];
328
+ return normalized
329
+ .replace(/^openclaw[-_]/, "")
330
+ .split(/[-_]+/)
331
+ .filter(Boolean)
332
+ .map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`)
333
+ .join(" ");
334
+ }
335
+
336
+ export function buildThreadTitle(ctx, normalizedMessages = []) {
337
+ const firstUserMessage = normalizedMessages.find(
338
+ (message) => message?.role === "user",
339
+ );
340
+ const userText = String(
341
+ firstUserMessage?.fullContent || firstUserMessage?.content || "",
342
+ )
343
+ .replace(/\s+/gu, " ")
344
+ .trim();
345
+ if (userText) return truncate(userText, MAX_THREAD_TITLE_CHARS);
346
+
347
+ const explicitChannel = ctx?.channel || ctx?.channelId;
348
+ const sessionKey = String(ctx?.sessionKey || "");
349
+ const sessionParts = sessionKey.split(":").filter(Boolean);
350
+ const sessionChannel =
351
+ sessionParts[0]?.toLowerCase() === "agent" ? sessionParts[2] : "";
352
+ const channel = humanizeChannel(explicitChannel || sessionChannel);
353
+ return channel ? `OpenClaw · ${channel}` : "OpenClaw conversation";
308
354
  }
309
355
 
310
356
  function sanitizeIdPart(input, max = 48) {
@@ -416,11 +462,11 @@ export async function appendOrCreateThread({
416
462
  const threadId = buildStableThreadId(event, ctx);
417
463
  const sessionKey = String(ctx?.sessionKey || ctx?.sessionId || "session");
418
464
  const sessionId = String(ctx?.sessionId || "").trim();
419
- const title = buildThreadTitle(ctx);
420
465
  const normalized = rawMessages
421
466
  .map((message) => normalizeRoleMessage(message, maxMessageChars))
422
467
  .filter(Boolean);
423
468
  if (normalized.length === 0) return;
469
+ const title = buildThreadTitle(ctx, normalized);
424
470
 
425
471
  const buildMessages = (resolvedMessageMode) =>
426
472
  normalized.map((message, index) => ({
@@ -662,7 +708,7 @@ export async function triageAndDistill({
662
708
 
663
709
  const distillResult = await client.distillThread({
664
710
  threadId: captureResult.threadId,
665
- title: buildThreadTitle(ctx),
711
+ title: buildThreadTitle(ctx, captureResult.normalized),
666
712
  content: conversationText,
667
713
  });
668
714
 
package/src/index.js CHANGED
@@ -78,6 +78,12 @@ export default {
78
78
  // - memory/context-engine slot routing for capture and recall
79
79
  const contextEngineSlot = api.config?.plugins?.slots?.contextEngine;
80
80
  const pluginsAllow = api.config?.plugins?.allow;
81
+ const pluginEntry = api.config?.plugins?.entries?.[api.id];
82
+ const toolsProfile = api.config?.tools?.profile;
83
+ const toolsAllow = api.config?.tools?.allow;
84
+ const toolsAlsoAllow = api.config?.tools?.alsoAllow;
85
+ const allowConversationAccess =
86
+ pluginEntry?.hooks?.allowConversationAccess === true;
81
87
  let contextEngineRegistered = false;
82
88
  let contextEngineRegistrationError = null;
83
89
  let corpusSupplementActive = false;
@@ -140,6 +146,10 @@ export default {
140
146
  contextEngineSlot,
141
147
  contextEngineIds: NOWLEDGE_MEM_CONTEXT_ENGINE_IDS,
142
148
  pluginsAllow,
149
+ toolsProfile,
150
+ toolsAllow,
151
+ toolsAlsoAllow,
152
+ allowConversationAccess,
143
153
  contextEngineRegistered,
144
154
  contextEngineRegistrationError,
145
155
  corpusSupplementActive,
@@ -27,10 +27,10 @@ export function createStatusTool(client, _logger, cfg, runtimeInfo = {}) {
27
27
  const lines = [];
28
28
  const details = {};
29
29
  const sources = cfg._sources || {};
30
+ const pluginId = "openclaw-nowledge-mem";
30
31
 
31
32
  // 0a. Plugin trust check — detect missing plugins.allow
32
33
  const pluginsAllow = runtimeInfo.pluginsAllow;
33
- const pluginId = "openclaw-nowledge-mem";
34
34
  details.pluginsAllow = !Array.isArray(pluginsAllow)
35
35
  ? "(not set)"
36
36
  : pluginsAllow.includes(pluginId)
@@ -64,7 +64,48 @@ export function createStatusTool(client, _logger, cfg, runtimeInfo = {}) {
64
64
  lines.push(`Plugin trust: ${pluginId} (allowlisted)`);
65
65
  }
66
66
 
67
- // 0b. Memory slot check — show current mode and options
67
+ // 0b. Effective tool policy. OpenClaw profiles are restrictive; the
68
+ // plugin id expands to this plugin's declared tool contract.
69
+ const toolsProfile = runtimeInfo.toolsProfile;
70
+ const toolsAllow = Array.isArray(runtimeInfo.toolsAllow)
71
+ ? runtimeInfo.toolsAllow
72
+ : null;
73
+ const toolsAlsoAllow = Array.isArray(runtimeInfo.toolsAlsoAllow)
74
+ ? runtimeInfo.toolsAlsoAllow
75
+ : null;
76
+ const profileRestrictsPlugins =
77
+ typeof toolsProfile === "string" && toolsProfile !== "full";
78
+ const pluginExplicitlyAllowed =
79
+ toolsAllow?.includes(pluginId) || toolsAlsoAllow?.includes(pluginId);
80
+ const toolPolicyBlocksPlugin =
81
+ (toolsAllow !== null && !toolsAllow.includes(pluginId)) ||
82
+ (profileRestrictsPlugins && !pluginExplicitlyAllowed);
83
+ details.toolsProfile = toolsProfile ?? "(not set)";
84
+ details.pluginToolsAllowed = !toolPolicyBlocksPlugin;
85
+ if (toolPolicyBlocksPlugin) {
86
+ lines.push(
87
+ `⚠ Tool policy does not expose the complete ${pluginId} tool contract.`,
88
+ );
89
+ if (toolsAllow !== null) {
90
+ lines.push(
91
+ ` Fix: preserve tools.allow and add "${pluginId}" to that list.`,
92
+ );
93
+ } else {
94
+ lines.push(
95
+ ` Fix: preserve tools.alsoAllow and add "${pluginId}" to that list.`,
96
+ );
97
+ }
98
+ lines.push(
99
+ " Do not list individual nowledge_mem_* tools; use the plugin id so the manifest contract stays complete.",
100
+ );
101
+ lines.push("");
102
+ } else if (toolsProfile) {
103
+ lines.push(
104
+ `Tool policy: ${toolsProfile} profile + ${pluginId} (complete contract allowed)`,
105
+ );
106
+ }
107
+
108
+ // 0c. Memory slot check — show current mode and options
68
109
  const memorySlot = runtimeInfo.memorySlot;
69
110
  const contextEngineSlot = runtimeInfo.contextEngineSlot;
70
111
  const contextEngineRegistered =
@@ -135,14 +176,20 @@ export function createStatusTool(client, _logger, cfg, runtimeInfo = {}) {
135
176
  lines.push("Memory slot: openclaw-nowledge-mem (active)");
136
177
  }
137
178
 
138
- // 0c. Capture routing — hooks vs context engine
179
+ // 0d. Capture routing — hooks vs context engine
139
180
  const ceActive =
140
181
  isNowledgeMemContextEngineSlot(ceSlot) && contextEngineRegistered;
182
+ const hookCaptureAllowed = runtimeInfo.allowConversationAccess === true;
183
+ details.allowConversationAccess = hookCaptureAllowed;
141
184
  const captureMode = !cfg.sessionDigest
142
185
  ? "disabled"
143
186
  : ceActive
144
- ? "context-engine+hooks"
145
- : "hooks";
187
+ ? hookCaptureAllowed
188
+ ? "context-engine+hooks"
189
+ : "context-engine"
190
+ : hookCaptureAllowed
191
+ ? "hooks"
192
+ : "blocked";
146
193
  details.captureMode = captureMode;
147
194
  if (ceActive) {
148
195
  if (ceSlot === NOWLEDGE_MEM_CONTEXT_ENGINE_COMPAT_ALIAS) {
@@ -158,7 +205,9 @@ export function createStatusTool(client, _logger, cfg, runtimeInfo = {}) {
158
205
  );
159
206
  }
160
207
  lines.push(
161
- " Thread capture runs through Context Engine afterTurn with hook fallback (agent_end, after_compaction, before_reset).",
208
+ hookCaptureAllowed
209
+ ? " Thread capture runs through Context Engine afterTurn with hook fallback (agent_end, after_compaction, before_reset)."
210
+ : " Thread capture runs through Context Engine afterTurn; the agent_end fallback is blocked by OpenClaw policy.",
162
211
  );
163
212
  } else if (isNowledgeMemContextEngineSlot(ceSlot)) {
164
213
  if (ceSlot === NOWLEDGE_MEM_CONTEXT_ENGINE_COMPAT_ALIAS) {
@@ -194,6 +243,14 @@ export function createStatusTool(client, _logger, cfg, runtimeInfo = {}) {
194
243
  if (!cfg.sessionDigest) {
195
244
  lines.push("Thread capture: disabled (sessionDigest=false)");
196
245
  } else {
246
+ if (!hookCaptureAllowed) {
247
+ lines.push(
248
+ "⚠ Hook fallback blocked: set plugins.entries.openclaw-nowledge-mem.hooks.allowConversationAccess=true.",
249
+ );
250
+ lines.push(
251
+ " OpenClaw requires this explicit grant before a non-bundled plugin can read agent_end conversation content.",
252
+ );
253
+ }
197
254
  lines.push(
198
255
  " Thread identity follows OpenClaw session lifecycle: /new or /reset starts a fresh thread, while compaction stays in the same thread.",
199
256
  );
@@ -228,38 +285,68 @@ export function createStatusTool(client, _logger, cfg, runtimeInfo = {}) {
228
285
  lines.push("CLI: not found (install with: pip install nmem-cli)");
229
286
  }
230
287
 
231
- // 3. Backend health
232
- let healthy = false;
288
+ // 3. Backend health. Memory tools and thread capture travel through
289
+ // different surfaces: most reads/writes use the nmem CLI, while thread
290
+ // auto-sync writes directly to the HTTP API so large transcripts do not
291
+ // become argv-sized blobs. Report them separately; otherwise `nmem status`
292
+ // can produce a false green for users whose conversation capture is broken.
293
+ let cliHealthy = false;
233
294
  try {
234
- healthy = await client.checkHealth();
295
+ cliHealthy = await client.checkHealth();
235
296
  } catch {
236
297
  // checkHealth already returns false on error
237
298
  }
238
- details.healthy = healthy;
299
+ details.cliBackendReachable = cliHealthy;
239
300
 
240
- if (healthy) {
241
- lines.push("Backend: reachable");
242
- // Fetch detailed health info
243
- try {
244
- const health = await client.apiJson(
245
- "GET",
246
- "/health",
247
- undefined,
248
- 5000,
301
+ lines.push(`CLI backend: ${cliHealthy ? "reachable" : "not reachable"}`);
302
+
303
+ let httpHealthy = false;
304
+ try {
305
+ const health = await client.apiJson("GET", "/health", undefined, 5000);
306
+ httpHealthy = true;
307
+ if (health.version) {
308
+ details.version = health.version;
309
+ lines.push(`Version: ${health.version}`);
310
+ }
311
+ if (health.database_connected !== undefined) {
312
+ details.databaseConnected = health.database_connected;
313
+ lines.push(
314
+ `Database: ${health.database_connected ? "connected" : "disconnected"}`,
249
315
  );
250
- if (health.version) {
251
- details.version = health.version;
252
- lines.push(`Version: ${health.version}`);
253
- }
254
- if (health.database_connected !== undefined) {
255
- details.databaseConnected = health.database_connected;
256
- lines.push(
257
- `Database: ${health.database_connected ? "connected" : "disconnected"}`,
258
- );
259
- }
260
- } catch {
261
- // Health endpoint not available on older backends
262
316
  }
317
+ } catch (err) {
318
+ const message = err instanceof Error ? err.message : String(err);
319
+ details.httpHealthError = message;
320
+ }
321
+ details.httpBackendReachable = httpHealthy;
322
+
323
+ let threadApiHealthy = false;
324
+ try {
325
+ await client.apiJson("GET", "/threads/sources", undefined, 5000);
326
+ threadApiHealthy = true;
327
+ } catch (err) {
328
+ const message = err instanceof Error ? err.message : String(err);
329
+ details.threadSyncHttpError = message;
330
+ }
331
+ details.threadSyncHttpReachable = threadApiHealthy;
332
+ details.healthy = cliHealthy && threadApiHealthy;
333
+
334
+ if (threadApiHealthy) {
335
+ lines.push("Thread sync HTTP API: reachable");
336
+ } else {
337
+ lines.push("Thread sync HTTP API: not reachable");
338
+ lines.push(
339
+ " Conversation capture writes to the Mem HTTP API. It will not sync while this is unreachable, even if memory tools still work through the CLI.",
340
+ );
341
+ if (details.threadSyncHttpError) {
342
+ lines.push(` Detail: ${details.threadSyncHttpError}`);
343
+ }
344
+ }
345
+
346
+ if (cliHealthy && threadApiHealthy) {
347
+ lines.push("Backend: reachable");
348
+ } else if (cliHealthy || threadApiHealthy || httpHealthy) {
349
+ lines.push("Backend: partially reachable");
263
350
  } else {
264
351
  lines.push("Backend: not reachable");
265
352
  if (remote) {