Skip to content

Commit 42e1e38

Browse files
authored
fix: rebrand viewer, fix embedding warning, fix memory_add/search order (#1446)
## Summary ### 1. Rebrand viewer from OpenClaw to MemTensor - Replace all 28 user-visible "OpenClaw" references with "MemTensor" in the memory viewer UI - Update `server.ts` branding replacement regex to match new text ### 2. Fix embedding warning banner - Banner now dismisses when a configured (non-local) embedding provider is detected - Previously the warning persisted forever after first page load due to `_embeddingWarningShown` flag ### 3. Fix memory_add/search order (critical) - **Problem**: Hermes calls `sync_all()` before `queue_prefetch_all()`, so the current turn's data was ingested before the next prefetch search ran. This caused every search to return the just-added turn content — redundant and wasting context window. - **Fix**: `sync_turn()` now defers ingest by stashing the data. `queue_prefetch()` picks it up and flushes AFTER the search completes. This ensures recalled memories never include the current turn. - `on_session_end()` flushes any remaining deferred ingest to prevent data loss. ## Changed files | File | Change | |------|--------| | `adapters/hermes/__init__.py` | Defer ingest, search-before-add ordering | | `src/viewer/html.ts` | Rebrand OpenClaw → MemTensor, fix embedding banner | | `src/viewer/server.ts` | Update branding regex | ## Test plan - [ ] Start `hermes chat`, send a message — verify logs show `search` before `add` - [ ] Verify search results don't contain the message just sent - [ ] Verify viewer title shows "MemTensor Memory" - [ ] Configure embedding model → warning banner disappears - [ ] Exit session → verify deferred ingest is flushed (no data loss)
2 parents 6a3900b + b2d71b4 commit 42e1e38

File tree

6 files changed

+186
-67
lines changed

6 files changed

+186
-67
lines changed

apps/memos-local-plugin/adapters/hermes/__init__.py

Lines changed: 89 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import json
1212
import logging
1313
import os
14+
import re
1415
import sys
1516
import threading
1617
from pathlib import Path
@@ -46,6 +47,40 @@
4647
}
4748

4849

50+
_TRIVIAL_PATTERNS = [
51+
re.compile(r'^\s*\{["\']?ok["\']?\s*:\s*true\s*\}\s*$', re.IGNORECASE),
52+
re.compile(r'^\s*\{["\']?success["\']?\s*:\s*true\s*\}\s*$', re.IGNORECASE),
53+
re.compile(r'^\s*\{["\']?status["\']?\s*:\s*["\']?ok["\']?\s*\}\s*$', re.IGNORECASE),
54+
re.compile(r'^Operation interrupted:', re.IGNORECASE),
55+
re.compile(r'^Error:', re.IGNORECASE),
56+
re.compile(r'waiting for model response.*elapsed', re.IGNORECASE),
57+
re.compile(r'^\s*$'),
58+
]
59+
60+
_MIN_CONTENT_LENGTH = 6
61+
62+
63+
def _is_trivial(text: str) -> bool:
64+
"""Return True if *text* carries no meaningful information for long-term memory."""
65+
if not text or len(text.strip()) < _MIN_CONTENT_LENGTH:
66+
return True
67+
stripped = text.strip()
68+
for pat in _TRIVIAL_PATTERNS:
69+
if pat.search(stripped):
70+
return True
71+
try:
72+
obj = json.loads(stripped)
73+
if isinstance(obj, dict) and len(obj) <= 2:
74+
keys = {k.lower() for k in obj}
75+
if keys <= {"ok", "success", "status", "result", "error", "message"}:
76+
vals = list(obj.values())
77+
if all(isinstance(v, (bool, type(None))) or (isinstance(v, str) and len(v) < 20) for v in vals):
78+
return True
79+
except (json.JSONDecodeError, TypeError):
80+
pass
81+
return False
82+
83+
4984
class MemTensorProvider(MemoryProvider):
5085
"""MemTensor semantic memory — recall across sessions via bridge daemon."""
5186

@@ -56,6 +91,7 @@ def __init__(self) -> None:
5691
self._prefetch_lock = threading.Lock()
5792
self._prefetch_thread: threading.Thread | None = None
5893
self._sync_thread: threading.Thread | None = None
94+
self._pending_ingest: tuple | None = None
5995

6096
@property
6197
def name(self) -> str:
@@ -122,7 +158,11 @@ def prefetch(self, query: str, *, session_id: str = "") -> str:
122158
return f"## Recalled Memories\n{result}"
123159

124160
def queue_prefetch(self, query: str, *, session_id: str = "") -> None:
161+
pending = self._pending_ingest
162+
self._pending_ingest = None
163+
125164
def _run():
165+
# 1) Search FIRST — before ingesting the current turn
126166
try:
127167
text = self._do_recall(query)
128168
if text:
@@ -131,6 +171,22 @@ def _run():
131171
except Exception as e:
132172
logger.debug("MemTensor queue_prefetch failed: %s", e)
133173

174+
# 2) Now flush the deferred ingest so data is stored for future turns
175+
if pending and self._bridge:
176+
try:
177+
from config import OWNER
178+
user_content, assistant_content, sid = pending
179+
messages = []
180+
if user_content:
181+
messages.append({"role": "user", "content": user_content})
182+
if assistant_content:
183+
messages.append({"role": "assistant", "content": assistant_content})
184+
if messages:
185+
self._bridge.ingest(messages, session_id=sid, owner=OWNER)
186+
self._bridge.flush()
187+
except Exception as e:
188+
logger.warning("MemTensor deferred sync_turn failed: %s", e)
189+
134190
self._prefetch_thread = threading.Thread(
135191
target=_run, daemon=True, name="memtensor-prefetch"
136192
)
@@ -172,30 +228,26 @@ def _do_recall(self, query: str) -> str:
172228
def sync_turn(
173229
self, user_content: str, assistant_content: str, *, session_id: str = ""
174230
) -> None:
231+
"""Queue turn data for deferred ingest.
232+
233+
Hermes calls sync_all() BEFORE queue_prefetch_all(), so ingesting
234+
immediately would let the next prefetch retrieve the just-added turn.
235+
Instead we stash the data and let queue_prefetch() flush it AFTER
236+
the search completes — ensuring search results never contain the
237+
current turn's content.
238+
"""
175239
if not self._bridge:
176240
return
177-
241+
if _is_trivial(user_content) and _is_trivial(assistant_content):
242+
logger.debug("sync_turn: skipping trivial turn (user=%r, assistant=%r)",
243+
user_content[:80] if user_content else "", assistant_content[:80] if assistant_content else "")
244+
return
245+
if _is_trivial(user_content):
246+
user_content = ""
247+
if _is_trivial(assistant_content):
248+
assistant_content = ""
178249
sid = session_id or self._session_id or "default"
179-
messages = [
180-
{"role": "user", "content": user_content},
181-
{"role": "assistant", "content": assistant_content},
182-
]
183-
184-
def _sync():
185-
try:
186-
from config import OWNER
187-
self._bridge.ingest(messages, session_id=sid, owner=OWNER)
188-
self._bridge.flush()
189-
except Exception as e:
190-
logger.warning("MemTensor sync_turn failed: %s", e)
191-
192-
if self._sync_thread and self._sync_thread.is_alive():
193-
self._sync_thread.join(timeout=5.0)
194-
195-
self._sync_thread = threading.Thread(
196-
target=_sync, daemon=True, name="memtensor-sync"
197-
)
198-
self._sync_thread.start()
250+
self._pending_ingest = (user_content, assistant_content, sid)
199251

200252
def get_tool_schemas(self) -> List[Dict[str, Any]]:
201253
return [MEMORY_SEARCH_SCHEMA]
@@ -256,6 +308,22 @@ def _write():
256308
def on_session_end(self, messages: List[Dict[str, Any]]) -> None:
257309
if not self._bridge:
258310
return
311+
# Flush any deferred ingest that hasn't been picked up by queue_prefetch
312+
pending = self._pending_ingest
313+
self._pending_ingest = None
314+
if pending:
315+
try:
316+
from config import OWNER
317+
user_content, assistant_content, sid = pending
318+
msgs = []
319+
if user_content:
320+
msgs.append({"role": "user", "content": user_content})
321+
if assistant_content:
322+
msgs.append({"role": "assistant", "content": assistant_content})
323+
if msgs:
324+
self._bridge.ingest(msgs, session_id=sid, owner=OWNER)
325+
except Exception as e:
326+
logger.debug("MemTensor deferred ingest on session end failed: %s", e)
259327
try:
260328
self._bridge.flush()
261329
except Exception as e:

apps/memos-local-plugin/adapters/hermes/config.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ def get_viewer_port() -> int:
5454
return VIEWER_PORT
5555

5656

57-
def _read_openclaw_model_config() -> dict:
58-
"""Read embedding/summarizer config from OpenClaw's openclaw.json as fallback."""
57+
def _read_host_model_config() -> dict:
58+
"""Read embedding/summarizer config from host agent's config as fallback."""
5959
home = os.environ.get("HOME", os.environ.get("USERPROFILE", ""))
6060
cfg_path = os.environ.get("OPENCLAW_CONFIG_PATH") or os.path.join(
6161
os.environ.get("OPENCLAW_STATE_DIR", os.path.join(home, ".openclaw")),
@@ -105,7 +105,7 @@ def get_bridge_config() -> dict:
105105
plugin_config["embedding"]["endpoint"] = endpoint
106106

107107
if "embedding" not in plugin_config:
108-
oc_config = _read_openclaw_model_config()
108+
oc_config = _read_host_model_config()
109109
if oc_config.get("embedding"):
110110
plugin_config["embedding"] = oc_config["embedding"]
111111
if oc_config.get("summarizer"):

apps/memos-local-plugin/adapters/hermes/install.sh

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ set -euo pipefail
1111
# - hermes-agent repository cloned locally
1212

1313
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
14-
MEMOS_OPENCLAW_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
14+
MEMOS_PLUGIN_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
1515

1616
# hermes-agent location: first argument or auto-detect
1717
# Priority: CLI arg > hermes runtime dir > repo clone
@@ -32,7 +32,7 @@ TARGET_DIR="$HERMES_REPO/plugins/memory/memtensor"
3232
echo "=== MemTensor Memory Plugin Installer (hermes-agent) ==="
3333
echo ""
3434
echo "Plugin source: $SCRIPT_DIR"
35-
echo "OpenClaw core: $MEMOS_OPENCLAW_DIR"
35+
echo "Plugin root: $MEMOS_PLUGIN_DIR"
3636
echo "Hermes repo: $HERMES_REPO"
3737
echo "Install target: $TARGET_DIR"
3838
echo ""
@@ -57,11 +57,11 @@ fi
5757

5858
echo "✓ Node.js $(node -v)"
5959

60-
# ─── Install memos-local-openclaw dependencies ───
60+
# ─── Install plugin dependencies ───
6161

6262
echo ""
63-
echo "Installing memos-local-openclaw dependencies..."
64-
cd "$MEMOS_OPENCLAW_DIR"
63+
echo "Installing plugin dependencies..."
64+
cd "$MEMOS_PLUGIN_DIR"
6565

6666
if command -v pnpm &>/dev/null; then
6767
pnpm install --frozen-lockfile 2>/dev/null || pnpm install
@@ -76,7 +76,7 @@ echo "✓ Dependencies installed"
7676

7777
# ─── Record bridge path for runtime discovery ───
7878

79-
BRIDGE_CTS="$MEMOS_OPENCLAW_DIR/bridge.cts"
79+
BRIDGE_CTS="$MEMOS_PLUGIN_DIR/bridge.cts"
8080
echo "$BRIDGE_CTS" > "$SCRIPT_DIR/bridge_path.txt"
8181

8282
if [ -f "$BRIDGE_CTS" ]; then

apps/memos-local-plugin/src/ingest/worker.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,40 @@ import { Summarizer } from "./providers";
77
import { findDuplicate, findTopSimilar } from "./dedup";
88
import { TaskProcessor } from "./task-processor";
99

10+
const TRIVIAL_CONTENT_RE = [
11+
/^\s*\{["']?ok["']?\s*:\s*true\s*\}\s*$/i,
12+
/^\s*\{["']?success["']?\s*:\s*true\s*\}\s*$/i,
13+
/^\s*\{["']?status["']?\s*:\s*["']?ok["']?\s*\}\s*$/i,
14+
/^Operation interrupted:/i,
15+
/waiting for model response.*elapsed/i,
16+
/^\s*$/,
17+
];
18+
const MIN_CONTENT_LENGTH = 6;
19+
20+
function isTrivialContent(text: string): boolean {
21+
if (!text || text.trim().length < MIN_CONTENT_LENGTH) return true;
22+
const s = text.trim();
23+
for (const re of TRIVIAL_CONTENT_RE) {
24+
if (re.test(s)) return true;
25+
}
26+
try {
27+
const obj = JSON.parse(s);
28+
if (obj && typeof obj === "object" && !Array.isArray(obj)) {
29+
const keys = Object.keys(obj);
30+
if (keys.length <= 2) {
31+
const lowerKeys = new Set(keys.map(k => k.toLowerCase()));
32+
const trivialKeys = new Set(["ok", "success", "status", "result", "error", "message"]);
33+
if ([...lowerKeys].every(k => trivialKeys.has(k))) {
34+
if (Object.values(obj).every((v: any) => typeof v === "boolean" || v === null || (typeof v === "string" && v.length < 20))) {
35+
return true;
36+
}
37+
}
38+
}
39+
}
40+
} catch { /* not JSON */ }
41+
return false;
42+
}
43+
1044
export class IngestWorker {
1145
private summarizer: Summarizer;
1246
private taskProcessor: TaskProcessor;
@@ -30,7 +64,14 @@ export class IngestWorker {
3064
}
3165

3266
enqueue(messages: ConversationMessage[]): void {
33-
const filtered = messages.filter((m) => !IngestWorker.isEphemeralSession(m.sessionKey));
67+
const filtered = messages.filter((m) => {
68+
if (IngestWorker.isEphemeralSession(m.sessionKey)) return false;
69+
if (isTrivialContent(m.content)) {
70+
this.ctx.log.debug(`Skipping trivial content: ${m.content.slice(0, 80)}`);
71+
return false;
72+
}
73+
return true;
74+
});
3475
if (filtered.length === 0) return;
3576
this.queue.push(...filtered);
3677
if (!this.processing) {

0 commit comments

Comments
 (0)