Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,36 @@ curl http://127.0.0.1:8420/health

---

### 3. Hermes (Windows native)

For a Windows-native Hermes install, run the bundled batch script from the
repository root in Command Prompt or PowerShell:

```powershell
$env:TDAI_LLM_API_KEY="your-api-key"
$env:TDAI_LLM_BASE_URL="https://api.openai.com/v1"
$env:TDAI_LLM_MODEL="gpt-4o"
.\scripts\setup-hermes-memory-tencentdb.bat
```

The script checks `node`, `npm`, Python, and Hermes, requires Node.js
`>=22.16.0`, runs `npm install --omit=dev` when Gateway dependencies are
missing, creates `%USERPROFILE%\.memory-tencentdb\memory-tdai`, copies the
provider to `%USERPROFILE%\.hermes\plugins\memory_tencentdb`, writes Gateway
environment variables to `%USERPROFILE%\.hermes\.env`, and starts the Gateway
before polling:

```powershell
curl.exe http://127.0.0.1:8420/health
```

If `%USERPROFILE%\.hermes\config.yaml` already exists, make sure it contains:

```yaml
memory:
provider: memory_tencentdb
```


## 🔒 Gateway Security (optional)

Expand Down
31 changes: 30 additions & 1 deletion README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -353,9 +353,38 @@ curl http://127.0.0.1:8420/health

> Provider 的完整参考(环境变量、故障排查、LLM 工具 schema、supervisor 行为)见 [`hermes-plugin/memory/memory_tencentdb/README.md`](./hermes-plugin/memory/memory_tencentdb/README.md),调整 supervisor / circuit-breaker 默认值之前请先读它。


---

### 3. Hermes(Windows 原生安装)

Windows 原生 Hermes 环境下,在仓库根目录用 Command Prompt 或 PowerShell
运行内置批处理脚本:

```powershell
$env:TDAI_LLM_API_KEY="your-api-key"
$env:TDAI_LLM_BASE_URL="https://api.openai.com/v1"
$env:TDAI_LLM_MODEL="gpt-4o"
.\scripts\setup-hermes-memory-tencentdb.bat
```

脚本会检查 `node`、`npm`、Python 和 Hermes,要求 Node.js `>=22.16.0`;
当 Gateway 依赖缺失时执行 `npm install --omit=dev`,创建
`%USERPROFILE%\.memory-tencentdb\memory-tdai`,复制插件到
`%USERPROFILE%\.hermes\plugins\memory_tencentdb`,把 Gateway 环境变量写入
`%USERPROFILE%\.hermes\.env`,随后启动 Gateway 并轮询:

```powershell
curl.exe http://127.0.0.1:8420/health
```

如果 `%USERPROFILE%\.hermes\config.yaml` 已存在,请确认包含:

```yaml
memory:
provider: memory_tencentdb
```


## 🔒 Gateway 安全配置(可选)

Hermes Gateway 监听 `:8420`,对外提供 capture / search / recall 的 HTTP 接口。新增两个开关,可以把它从“开放的本地 sidecar”切换为“需要鉴权的网络服务”。**两个开关默认都关闭,已有部署的行为不变。**
Expand Down
88 changes: 72 additions & 16 deletions hermes-plugin/memory/memory_tencentdb/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,56 @@ Hermes scans two locations for memory providers, in precedence order (see
collision.
2. **User-installed** — `$HERMES_HOME/plugins/<name>/`, where
`$HERMES_HOME` defaults to `~/.hermes` (see
`hermes_constants.get_hermes_home()`). This path is for third-party
providers; we don't use it for memory_tencentdb.
`hermes_constants.get_hermes_home()`). This path is used by the Windows
native `.bat` installer and by third-party provider installs.

**The trailing directory name must be exactly `memory_tencentdb`** — Hermes
uses that directory name as the provider key; it must match
`plugin.yaml::name` and the value of `memory.provider` in `config.yaml`.
(The hyphenated form `memory-tencentdb` is a *config-side alias*, not a
valid directory name.)

### Windows native install

On Windows, use the bundled batch script from the memory-tencentdb repository
root:

```cmd
set TDAI_LLM_API_KEY=sk-...
set TDAI_LLM_BASE_URL=https://api.openai.com/v1
set TDAI_LLM_MODEL=gpt-4o
scripts\setup-hermes-memory-tencentdb.bat
```

PowerShell equivalent:

```powershell
$env:TDAI_LLM_API_KEY="sk-..."
$env:TDAI_LLM_BASE_URL="https://api.openai.com/v1"
$env:TDAI_LLM_MODEL="gpt-4o"
.\scripts\setup-hermes-memory-tencentdb.bat
```

The script performs the minimal native setup:

- checks `node`, `npm`, Python, and the optional `hermes` command;
- requires Node.js `>=22.16.0`;
- runs `npm install --omit=dev` when `node_modules` is missing;
- creates `%USERPROFILE%\.memory-tencentdb\memory-tdai`;
- sets `TDAI_LLM_*` and `MEMORY_TENCENTDB_GATEWAY_*` in the current process
and writes them to `%USERPROFILE%\.hermes\.env`;
- copies this provider to `%USERPROFILE%\.hermes\plugins\memory_tencentdb`;
- creates `%USERPROFILE%\.hermes\config.yaml` with
`memory.provider: memory_tencentdb` when the file does not exist, or prints
the exact YAML to add when it already exists;
- starts the Gateway and polls `GET /health`.

After it finishes, verify the sidecar directly:

```cmd
curl.exe http://127.0.0.1:8420/health
```

Pick one of the two installation styles:

**Install A — symlink (recommended for developers working on both repos
Expand Down Expand Up @@ -140,14 +181,20 @@ memory:
### 2. Provide Gateway runtime + LLM credentials

At minimum the Gateway needs an OpenAI-compatible endpoint for L1/L2/L3
extraction. Set these in the Hermes process environment:
extraction. Set the Gateway-native variables in the Hermes process
environment:

```bash
export MEMORY_TENCENTDB_LLM_API_KEY="sk-..."
export MEMORY_TENCENTDB_LLM_BASE_URL="https://api.openai.com/v1" # optional
export MEMORY_TENCENTDB_LLM_MODEL="gpt-4o" # optional
export TDAI_LLM_API_KEY="sk-..."
export TDAI_LLM_BASE_URL="https://api.openai.com/v1" # optional
export TDAI_LLM_MODEL="gpt-4o" # optional
```

When the provider supervises the Gateway, it also accepts the legacy
Hermes-side aliases `MEMORY_TENCENTDB_LLM_API_KEY`,
`MEMORY_TENCENTDB_LLM_BASE_URL`, and `MEMORY_TENCENTDB_LLM_MODEL` and mirrors
them into `TDAI_LLM_*` for the child process.

### 3. Start the Gateway

You have three options; pick whichever fits your deployment.
Expand Down Expand Up @@ -229,18 +276,27 @@ The old `MEMORY_TENCENTDB_DATA_DIR` env var is no longer read — it was never
consumed by the Gateway anyway (names did not match), so removing it just
eliminates a silent no-op.

### Gateway LLM (consumed by the Node sidecar, not by this provider)
### Gateway LLM (consumed by the Node sidecar)

| Variable | Default | Description |
|----------|---------|-------------|
| `TDAI_LLM_API_KEY` | — | LLM API key (required for L1/L2/L3) |
| `TDAI_LLM_BASE_URL` | `https://api.openai.com/v1` | OpenAI-compatible API base URL |
| `TDAI_LLM_MODEL` | `gpt-4o` | Model name |

When the provider starts the Gateway subprocess, it mirrors the legacy
Hermes aliases below into the Gateway-native names above if the `TDAI_*`
variables are not already set:

| Variable | Default | Description |
|-----------------------------------|------------------------------|-------------------------------------|
| `MEMORY_TENCENTDB_LLM_API_KEY` | — | LLM API key (required for L1/L2/L3) |
| `MEMORY_TENCENTDB_LLM_BASE_URL` | `https://api.openai.com/v1` | OpenAI-compatible API base URL |
| `MEMORY_TENCENTDB_LLM_MODEL` | `gpt-4o` | Model name |
| Alias | Mirrored to |
|-------|-------------|
| `MEMORY_TENCENTDB_LLM_API_KEY` | `TDAI_LLM_API_KEY` |
| `MEMORY_TENCENTDB_LLM_BASE_URL` | `TDAI_LLM_BASE_URL` |
| `MEMORY_TENCENTDB_LLM_MODEL` | `TDAI_LLM_MODEL` |

> ⚠️ Only `MEMORY_TENCENTDB_*` env vars are honored by this provider for the
> Gateway location and LLM credentials. Data-directory resolution is
> deliberately delegated to the Gateway via `TDAI_DATA_DIR` (see above) so
> the provider and the Gateway can never disagree about where L0~L3 live.
> Data-directory resolution is deliberately delegated to the Gateway via
> `TDAI_DATA_DIR` (see above) so the provider and the Gateway can never
> disagree about where L0~L3 live.

## LLM Tools

Expand Down
42 changes: 29 additions & 13 deletions hermes-plugin/memory/memory_tencentdb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,14 +186,14 @@ def _discover_gateway_cmd() -> Optional[str]:
``~/tdai-memory-openclaw-plugin`` and
``~/.hermes/plugins/tdai-memory-openclaw-plugin``).

Returns a ready-to-``Popen`` command string wrapping a ``sh -c`` that
``cd``-s into the plugin root before exec-ing ``pnpm exec tsx
src/gateway/server.ts``. The ``cd`` is required because ``tsx`` is
installed under ``<plugin-root>/node_modules`` and Node's ESM resolver
searches ``package.json`` from the cwd upward — if we launched ``tsx``
with the hermes-agent cwd, resolution would fail with
``ERR_MODULE_NOT_FOUND``. Using ``sh -c`` keeps the supervisor's
``shlex.split`` + ``Popen(argv)`` contract intact (no ``shell=True``).
Returns a ready-to-``Popen`` command string that ``cd``-s into the plugin
root before exec-ing the Gateway. The ``cd`` is required because ``tsx``
is installed under ``<plugin-root>/node_modules`` and Node's ESM resolver
searches ``package.json`` from the cwd upward — if we launched from the
hermes-agent cwd, resolution would fail with ``ERR_MODULE_NOT_FOUND``.
POSIX uses ``sh -c`` so the supervisor can keep its ``shlex.split`` +
``Popen(argv)`` path. Windows returns an explicit ``cmd /d /s /c ...``
command and the supervisor starts it with ``shell=True``.

Returns ``None`` if no ``server.ts`` candidate exists. The function never
raises: supervisor-side validation will surface a friendly warning if the
Expand Down Expand Up @@ -231,6 +231,18 @@ def _discover_gateway_cmd() -> Optional[str]:
"(override with MEMORY_TENCENTDB_GATEWAY_CMD)",
candidate,
)
if os.name == "nt":
# cmd.exe needs /d so autorun hooks do not mutate the
# environment, /s for predictable quote stripping, and
# /c so Hermes can supervise the process lifetime.
# `node --import tsx/esm` mirrors the packaged Windows
# bootstrap script and avoids relying on npx/pnpm.
inner = (
f'cd /d "{str(plugin_root)}" && '
"node --import tsx/esm src\\gateway\\server.ts"
)
return f'cmd /d /s /c "{inner}"'

# shlex.quote guards against spaces / shell metachars in paths.
# The inner command mirrors start-memory-tencentdb-gateway.sh:
# cd <plugin-root> && exec pnpm exec tsx src/gateway/server.ts
Expand Down Expand Up @@ -971,15 +983,19 @@ def shutdown(self) -> None:
except Exception as e:
logger.debug("memory-tencentdb session end failed: %s", e)

# Note: do NOT shut down the supervisor/Gateway here — it may serve
# other sessions. The Gateway manages its own lifecycle.
# We *do* drop our reference to the supervisor so any in-flight
# _try_recover_gateway() call sees self._supervisor is None and
# bails out instead of resurrecting a released provider.
# Shut down only the Gateway process this supervisor started itself.
# External Gateways are safe: GatewaySupervisor.shutdown() is a no-op
# when it never spawned a child (self._process is None).
supervisor = self._supervisor
self._client = None
self._gateway_available = False
self._initialized = False
self._supervisor = None
if supervisor is not None:
try:
supervisor.shutdown()
except Exception as e:
logger.warning("memory-tencentdb supervisor shutdown failed: %s", e)

# -- Tools ----------------------------------------------------------------

Expand Down
77 changes: 67 additions & 10 deletions hermes-plugin/memory/memory_tencentdb/supervisor.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import logging
import os
import shlex
import signal
import subprocess
import time
from typing import IO, Optional
Expand Down Expand Up @@ -168,6 +169,21 @@ def ensure_running(self) -> bool:
env = os.environ.copy()
env["MEMORY_TENCENTDB_GATEWAY_PORT"] = str(self._port)
env["MEMORY_TENCENTDB_GATEWAY_HOST"] = self._host
# The Node Gateway reads TDAI_GATEWAY_{HOST,PORT}. Keep the
# MEMORY_TENCENTDB_* names for the Python provider contract, but
# also populate the Gateway-native names unless the operator set
# them explicitly.
env.setdefault("TDAI_GATEWAY_PORT", str(self._port))
env.setdefault("TDAI_GATEWAY_HOST", self._host)
# Hermes-facing LLM env names predate the Gateway's TDAI_LLM_*
# names. Mirror them into the child process so Windows-native
# Hermes installs do not have to set both sets by hand.
if not env.get("TDAI_LLM_API_KEY") and env.get("MEMORY_TENCENTDB_LLM_API_KEY"):
env["TDAI_LLM_API_KEY"] = env["MEMORY_TENCENTDB_LLM_API_KEY"]
if not env.get("TDAI_LLM_BASE_URL") and env.get("MEMORY_TENCENTDB_LLM_BASE_URL"):
env["TDAI_LLM_BASE_URL"] = env["MEMORY_TENCENTDB_LLM_BASE_URL"]
if not env.get("TDAI_LLM_MODEL") and env.get("MEMORY_TENCENTDB_LLM_MODEL"):
env["TDAI_LLM_MODEL"] = env["MEMORY_TENCENTDB_LLM_MODEL"]
# Note: we deliberately do NOT inject TDAI_GATEWAY_API_KEY into
# the child's env from here. Whether the Gateway enforces auth is
# the operator's call — they configure it on the Gateway side
Expand Down Expand Up @@ -203,13 +219,26 @@ def ensure_running(self) -> bool:
stdout_target = subprocess.DEVNULL
stderr_target = subprocess.DEVNULL

self._process = subprocess.Popen(
shlex.split(self._gateway_cmd),
env=env,
stdout=stdout_target,
stderr=stderr_target,
start_new_session=True, # Detach from parent process group
)
if os.name == "nt":
creationflags = 0
if hasattr(subprocess, "CREATE_NEW_PROCESS_GROUP"):
creationflags |= subprocess.CREATE_NEW_PROCESS_GROUP
self._process = subprocess.Popen(
self._gateway_cmd,
shell=True,
env=env,
stdout=stdout_target,
stderr=stderr_target,
creationflags=creationflags,
)
else:
self._process = subprocess.Popen(
shlex.split(self._gateway_cmd),
env=env,
stdout=stdout_target,
stderr=stderr_target,
start_new_session=True, # Detach from parent process group
)
except Exception as e:
logger.error("Failed to start memory-tencentdb Gateway: %s", e)
self._close_log_handles()
Expand Down Expand Up @@ -309,20 +338,48 @@ def shutdown(self) -> None:
logger.info("Shutting down memory-tencentdb Gateway...")

try:
# Send SIGTERM for graceful shutdown
self._process.terminate()
if os.name == "nt":
self._terminate_windows_process_tree(self._process.pid)
else:
# Send SIGTERM for graceful shutdown.
self._process.terminate()
try:
self._process.wait(timeout=10)
except subprocess.TimeoutExpired:
logger.warning("memory-tencentdb Gateway did not exit in 10s, sending SIGKILL")
self._process.kill()
if os.name == "nt":
self._kill_windows_process_tree(self._process.pid)
else:
self._process.kill()
self._process.wait(timeout=5)
except Exception as e:
logger.warning("Error shutting down memory-tencentdb Gateway: %s", e)
finally:
self._process = None
self._close_log_handles()

def _terminate_windows_process_tree(self, pid: int) -> None:
"""Terminate the shell-owned Windows process tree for the Gateway."""
try:
self._process.send_signal(signal.CTRL_BREAK_EVENT) # type: ignore[union-attr]
except Exception:
pass
subprocess.run(
["taskkill", "/T", "/PID", str(pid)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=False,
)

def _kill_windows_process_tree(self, pid: int) -> None:
"""Force-kill the shell-owned Windows process tree for the Gateway."""
subprocess.run(
["taskkill", "/F", "/T", "/PID", str(pid)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=False,
)

@property
def client(self) -> MemoryTencentdbSdkClient:
"""Get the HTTP client for making API calls."""
Expand Down
Loading