Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
3bf3ead
fix(memos-local-plugin): prevent orphan episode scan from closing act…
Starfie1d1272 Apr 27, 2026
328d1a5
fix(memos-local-plugin): prevent bridge process flood and crash on He…
Starfie1d1272 Apr 27, 2026
27d64eb
fix(memos-local-plugin): read bridge version from package.json instea…
Starfie1d1272 Apr 27, 2026
f2571e7
Update apps/memos-local-plugin/agent-contract/memory-core.ts
Starfie1d1272 Apr 27, 2026
68bfe16
Update apps/memos-local-plugin/core/session/manager.ts
Starfie1d1272 Apr 27, 2026
bfee04d
fix(memos-local-plugin): reject deletion of open episodes (409 conflict)
Starfie1d1272 Apr 27, 2026
23d0e89
fix(memos-local-plugin): connect Hermes provider to daemon bridge via…
Starfie1d1272 Apr 28, 2026
dc985cf
fix(memos-local-plugin): implement TCP transport for daemon bridge; a…
Starfie1d1272 Apr 28, 2026
948adff
fix(memos-local-plugin): address Copilot review round 2 — TCP fallbac…
Starfie1d1272 Apr 28, 2026
e09f116
fix: reconnect FD leak, tcp close race, deleteEpisode non-optional
Starfie1d1272 Apr 28, 2026
34bdb43
fix: tcp listen error handling, tcp_host fallback, delete response shape
Starfie1d1272 Apr 28, 2026
7e08cf7
fix: shutdown re-entrancy, JSON-RPC method validation, socket error c…
Starfie1d1272 Apr 28, 2026
981a1db
fix: remove duplicate tcpServer declaration
Starfie1d1272 Apr 28, 2026
a641b1d
fix: socket destroy guard, deleteClosedEpisode helper
Starfie1d1272 Apr 28, 2026
0fe7906
fix: TCP cleanup on failure, error listener, daemon guard, BridgeErro…
Starfie1d1272 Apr 28, 2026
8abb30b
test(memos-local-plugin): add orphan init and deleteEpisode unit tests
Starfie1d1272 Apr 28, 2026
700f006
fix(memos-local-plugin): close _rfile leak, PID path via MEMOS_HOME, …
Starfie1d1272 Apr 28, 2026
3fa7375
chore(memos-local-plugin): sync package-lock.json to 2.0.0-beta.1
Starfie1d1272 Apr 28, 2026
f191b15
fix: reduce empty episodes, finalize idle episodes, filter Hermes aut…
Starfie1d1272 Apr 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 33 additions & 6 deletions apps/memos-local-plugin/adapters/hermes/memos_provider/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
import contextlib
import json
import logging
import re
import sys
import threading
import time
Expand All @@ -62,7 +63,7 @@
if str(_PLUGIN_DIR) not in sys.path:
sys.path.insert(0, str(_PLUGIN_DIR))

from bridge_client import MemosBridgeClient # noqa: E402
from bridge_client import BridgeError, MemosBridgeClient # noqa: E402
from daemon_manager import ensure_bridge_running, shutdown_bridge # noqa: E402


Expand Down Expand Up @@ -142,7 +143,13 @@ def initialize(self, session_id: str, **kwargs: Any) -> None: # type: ignore[ov
logger.warning("MemOS: failed to start bridge — %s", err)
return
try:
self._bridge = MemosBridgeClient()
# Try TCP mode first (connect to daemon bridge).
# Falls back to stdio (spawn subprocess) if daemon isn't running.
try:
self._bridge = MemosBridgeClient(tcp_host="127.0.0.1", tcp_port=18911)
except BridgeError:
Comment on lines +146 to +150
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not an issue: the orchestrator's onTurnEnd() (memory-core.ts:655) resolves episodeId via openEpisodeBySession.get(sessionId) — the in-memory cache populated by turn.start. The adapter's _episode_id is only a fallback that is never reached in normal flow.

logger.info("MemOS: TCP daemon not available, falling back to stdio bridge")
self._bridge = MemosBridgeClient()
resp = self._bridge.request(
"session.open",
{
Expand All @@ -156,12 +163,9 @@ def initialize(self, session_id: str, **kwargs: Any) -> None: # type: ignore[ov
},
)
self._session_id = resp.get("sessionId") or session_id
ep = self._bridge.request("episode.open", {"sessionId": self._session_id})
self._episode_id = ep.get("episodeId", "")
logger.info(
"MemOS: bridge ready session=%s episode=%s platform=%s",
"MemOS: bridge ready session=%s platform=%s",
self._session_id,
self._episode_id,
self._platform,
)
except Exception as err:
Expand Down Expand Up @@ -455,6 +459,10 @@ def on_session_end(self, messages: list[dict[str, Any]]) -> None: # type: ignor
if pending:
with contextlib.suppress(Exception):
self._turn_end(*pending)
# Close the episode and session so the core stamps closure metadata
# (e.g. session.meta.closedAt). In TCP mode this ensures the daemon
# can distinguish "normal shutdown" from "abrupt disconnect" on
# restart, preventing orphan retention.
with contextlib.suppress(Exception):
self._bridge.request("episode.close", {"episodeId": self._episode_id})
Comment on lines +463 to 467
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not an issue: the episode.close call is wrapped in contextlib.suppress(Exception) and will silently no-op on empty id. The actual episode finalization is handled by autoFinalizeStaleTasks() in the pipeline.

with contextlib.suppress(Exception):
Expand Down Expand Up @@ -488,6 +496,17 @@ def _turn_start(self, query: str, *, session_id: str = "") -> str:
return ""
return f"## Recalled Memories\n{context}"

# Hermes injects a structured auto-skill evaluation prompt at task end:
# "Review the conversation above and consider whether a skill should
# be saved or updated. Work in this order… SURVEY … THINK CLASS-FIRST …"
# Capturing this system-level scaffolding as conversation content pollutes
# memory search, task summaries, and downstream skill generation.
_AUTO_SKILL_EVAL_RE = re.compile(
r"^Review the conversation above and consider whether a "
r"skill should be saved or updated\.",
re.MULTILINE,
)
Comment thread
Starfie1d1272 marked this conversation as resolved.

def _turn_end(
self,
user_content: str,
Expand All @@ -497,6 +516,14 @@ def _turn_end(
) -> None:
if not self._bridge:
return
# Strip Hermes auto-skill evaluation blocks from the assistant
# response. When the header phrase is present the entire remainder
# of the message is system-generated scaffolding, not user content.
m = self._AUTO_SKILL_EVAL_RE.search(assistant_content)
if m:
assistant_content = assistant_content[: m.start()].strip()
if not assistant_content.strip() and not user_content.strip():
return
self._bridge.request(
"turn.end",
{
Expand Down
Loading