Fixes#98
Return { latestCursor: cursor } instead of void when VectorStore or
JSONL pipeline returns zero records. Add cold-start guard in pipeline
manager to initialize last_extraction_updated_time on first run.
Previously the L2 runner performed a full table scan on every invocation
because the cursor never advanced past the initial state.
Signed-off-by: Oxygen56 <1391083091@qq.com>
Add unit tests for resolveApiKeyFromAuthProfile covering the four
resolution branches: api_key profile returns the plaintext key,
oauth/token-only providers return undefined, providers with no profile
return undefined, and keyRef-only (no plaintext) profiles are skipped.
Also covers the older-host case where the provider-auth SDK subpath is
unavailable.
To keep the test self-contained (no real OpenClaw install or on-disk
auth-profile store), resolveApiKeyFromAuthProfile gains an optional
fourth parameter to inject a fake provider-auth SDK loader. Production
callers pass three arguments and keep loading the real SDK via require.
Signed-off-by: MicroGrey <changyuhang@outlook.com>
Three config fields had comments that disagreed with the code defaults:
- maxScenes: comment said 20, code uses 15
- l2DelayAfterL1Seconds: comment said 90, code uses 10
- report.enabled: comment said true, code uses false
Closes#161
Co-authored-by: jackyangjie <yangjie@qq.com>
* docs(openclaw): recommend exact version install
Signed-off-by: yangjj-iso <3202137046@qq.com>
* Revise OpenClaw plugin installation instructions
Updated installation instructions for OpenClaw plugin to remove version specification and added upgrade command.
* Revise TencentDB memory plugin installation instructions
Updated installation instructions for the TencentDB memory plugin to remove the specific version and added a note on using the OpenClaw command for upgrades.
---------
Signed-off-by: yangjj-iso <3202137046@qq.com>
Co-authored-by: 十五便士 <95488710+Maxwell-Code07@users.noreply.github.com>
* docs(hermes): document plugging memory-tencentdb into an existing Hermes
The Hermes section in README.md / README_CN.md only documented the
all-in-one Docker image, which is the wrong starting point when the
user already has hermes-agent installed. They don't want to throw
away their setup just to get memory; they want to drop the provider
in next to the other built-in providers.
The full reference already exists at
`hermes-plugin/memory/memory_tencentdb/README.md` (auto-discovery
paths, env vars, supervisor behaviour, troubleshooting). What's
missing is the discoverable entry point at the top-level README.
Restructure section 2 into two parallel paths:
- 2.A Docker — the existing one-command flow, unchanged.
- 2.B Plug into an existing Hermes (no Docker) — new. Covers:
- installing the npm package so the Gateway source is on disk
- symlink vs copy of the Python provider into
`<hermes-agent-checkout>/plugins/memory/memory_tencentdb/`
(with the "underscore not hyphen" trap called out)
- the `memory.provider: memory_tencentdb` line in `~/.hermes/config.yaml`
- the three `MEMORY_TENCENTDB_LLM_*` env vars
- the three Gateway-start options (auto-discovery / explicit
GATEWAY_CMD / run-yourself), with auto-discovery as default
- a `curl /health` verification step
- a link to the in-repo provider README for the full reference
Docs-only — no code change. Existing Docker users see no difference.
Fixes#69
Signed-off-by: 李冠辰 <liguanchen@xiaomi.com>
* docs(hermes): document tdai gateway config file
* update README_CN.md file
* Fix formatting in README_CN.md for clarity
* Clarify Hermes plugin installation instructions
Updated the README to clarify installation instructions for the Hermes plugin, including Docker usage and configuration steps.
---------
Signed-off-by: 李冠辰 <liguanchen@xiaomi.com>
Co-authored-by: 十五便士 <95488710+Maxwell-Code07@users.noreply.github.com>
When offload.model references a provider whose apiKey is managed via
OpenClaw's auth-profiles mechanism (the recommended default, e.g. set up
with 'openclaw auth'), the local-llm initializer previously only read
models.providers[provider].apiKey in the config tree. If the key was
absent there, LocalLlmClient construction was skipped and L1/L1.5/L2
were disabled with a misleading "missing apiKey" error -- even though the
key existed and the main agent model worked fine.
Fix: add src/offload/auth-profile-key.ts, a synchronous helper that
loads the auth-profile store via openclaw/plugin-sdk/provider-auth and
returns the first api_key-type credential for the provider. The helper
is guarded with try/catch so older OpenClaw versions that do not expose
this SDK subpath degrade silently to the previous behavior.
In index.ts, the apiKey lookup becomes:
providerCfg?.apiKey ?? resolveApiKeyFromAuthProfile(api, providerKey, logger)
registerOffload() keeps its synchronous contract; no race is introduced
around backendClient assignment.
Closes#90
Signed-off-by: MicroGrey <changyuhang@outlook.com>
* feat(embedding): support ZeroEntropy native embed API
ZeroEntropy's embedding service is reachable but not OpenAI-shaped:
its endpoint is POST /v1/models/embed (not /v1/embeddings), the
request body requires an `input_type` ("query" or "document") and
rejects an explicit `dimensions` field, and the response is wrapped
as `{results: [{embedding}]}` instead of OpenAI's
`{data: [{index, embedding}]}`.
Branch on `provider === "zeroentropy"` inside OpenAIEmbeddingService
rather than introducing a new service class:
- `/models/embed` is used instead of `/embeddings` (also honoured
by the qclaw proxy path so users can still front it).
- request body sets `input_type: "query"` and omits `dimensions`
(zembed-1 is fixed-dimension and rejects explicit overrides).
- response parsing decodes the `results[]` envelope and preserves
input order via array index rather than OpenAI's `index` field.
`openclaw.plugin.json` updates the `embedding.provider` description to
list `zeroentropy` as a recognized value.
Everything else (timeout, retries, batching, char-cap truncation,
sanitize+normalize) is shared with the OpenAI path — only the wire
format changes.
Fixes#68
Signed-off-by: 李冠辰 <liguanchen@xiaomi.com>
* refactor(embedding): split ZeroEntropy into its own EmbeddingService
Addresses @Xuruida's review feedback on PR #137: instead of branching
OpenAIEmbeddingService._callApi on `providerName === "zeroentropy"`,
extract the shared HTTP layer and give ZeroEntropy its own class.
This keeps OpenAIEmbeddingService focused on the OpenAI-compatible
wire format and stops provider-specific protocol differences from
accumulating inside a single class.
Changes:
- New module-level helpers (provider-agnostic):
* `truncateEmbeddingInputs(texts, maxInputChars, logger)` — was
previously a private method duplicated logic-side.
* `postEmbeddingRequest({fetchUrl, headers, body, timeoutMs})` —
owns fetch + AbortController timeout + exponential-backoff
retries + the EmbeddingApiError non-retry rule for 4xx (except
429). Returns the parsed JSON; callers own response parsing.
- `OpenAIEmbeddingService._callApi` is back to OpenAI-only: builds
`{input, model, dimensions?}`, posts to `/embeddings`, parses
`{data: [{index, embedding}]}` with index-sort. The qclaw proxy
branch is unchanged. No more `isZeroEntropy` reads.
- New `ZeroEntropyEmbeddingService implements EmbeddingService`:
- Same `OpenAIEmbeddingConfig` shape (baseUrl / apiKey / model /
dimensions / maxInputChars / timeoutMs are identical on the
wire), but encodes ZeroEntropy's wire format independently:
posts to `${baseUrl}/models/embed`, sends `input_type: "query"`,
omits `dimensions` (zembed-1 is fixed-dim), parses
`{results: [{embedding}]}` preserving array order.
- Doesn't carry the OpenAI-only knobs (`sendDimensions`,
`proxyUrl`, qclaw) so the class surface stays small.
- `createEmbeddingService` factory: new `provider === "zeroentropy"`
branch placed before the OpenAI-compatible catch-all so it wins
the dispatch.
Behaviour preserved:
- All existing OpenAI / DeepSeek / Azure / qclaw deployments hit
exactly the same wire calls as before this PR was opened.
- ZeroEntropy wire format (URL + body + response) matches the
original PR's branched implementation byte for byte.
- Timeout, retry, error mapping, batch splitting, char-cap
truncation, sanitize+normalize are shared via the module helpers
so the two services can't diverge by accident.
Signed-off-by: 李冠辰 <liguanchen@xiaomi.com>
* fix(embedding): forward dimensions to ZeroEntropy when sendDimensions
Addresses @Xuruida's follow-up review on PR #137. The previous commit
silently dropped `dimensions` from every ZeroEntropy request based on
the wrong claim that zembed-1 is fixed-dim.
Verified against
https://docs.zeroentropy.dev/api-reference/models/embed: `dimensions`
is an optional integer; for zembed-1 the accepted set is
[2560, 1280, 640, 320, 160, 80, 40] (Matryoshka truncation). Not
forwarding it left users no way to pick a smaller dim end-to-end —
the wire payload always used the model default while the Float32Array
return type and getDimensions() still reported config.dimensions, so
the runtime vector size and the configured one could disagree.
- ZeroEntropyEmbeddingService now mirrors OpenAIEmbeddingService:
reads `config.sendDimensions ?? true` into a `sendDimensions` field
and emits `dimensions: this.dims` in the request body when set.
- Class docstring corrected — the "rejects explicit overrides" line
is replaced with the documented Matryoshka set. Server still
rejects out-of-set values, but that's a config choice the user
owns; we forward verbatim instead of clamping silently.
Signed-off-by: 李冠辰 <liguanchen@xiaomi.com>
---------
Signed-off-by: 李冠辰 <liguanchen@xiaomi.com>
truncateRecallLine capped lines with line.length and line.slice, which
count and index by UTF-16 code unit. When recall.maxCharsPerMemory or
recall.maxTotalRecallChars is set and the cut lands between the halves of
a surrogate pair, the line keeps a lone surrogate that becomes U+FFFD once
UTF-8 encoded for the request, corrupting any non-BMP character (emoji,
CJK Ext-B) in the injected context. Slice on Array.from(line) code points
instead. Both recall call sites route through this function. Same class as
the sanitizeText fix in #31, reintroduced by the #71 budget path.
Signed-off-by: Akhilesh Arora <akhildawra@gmail.com>
更新 README 中的微信社群二维码图片为最新版本,方便用户扫码加入社群。
Update the WeChat community QR code in README to the latest version, making it easier for users to join the community.
共替换 4 处二维码图片引用,仅修改图片 src 地址,保持原有显示尺寸(width="360" / width="200")和布局(居中对齐)不变。
* refactor: unify duplicate Logger interfaces into single canonical type
23 files had local Logger/StoreLogger/PluginLogger/PipelineLogger/RunnerLogger/ExtractorLogger/TriggerLogger
interface definitions with identical shapes ({debug?, info, warn, error}).
Replace all with imports from the canonical `Logger` in `src/core/types.ts`:
- 17 files: local `interface Logger` → `import type { Logger }`
- 6 files: named variants (StoreLogger, PluginLogger, etc.) → `type X = Logger` alias
Two intentionally different interfaces are preserved:
- `CheckpointLogger` (only info + optional warn) — different contract
- `ensure-hook-policy.ts` Logger (no error) — different contract
Net: -120 lines, zero behavioral change.
Signed-off-by: yuanrengu <heyonggang0811@126.com>
* docs: update Logger JSDoc to reflect canonical relationship
StoreLogger, PluginLogger, etc. are now type aliases of Logger, not the
other way around. Update the comment to clarify this inverted relationship.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Signed-off-by: yuanrengu <heyonggang0811@126.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add `recall.maxCharsPerMemory` and `recall.maxTotalRecallChars` with defaults of `0`, which do not alter existing behavior. Users can opt in by setting positive values to cap injected memory context.
- Apply the budget after L1 search and before `<relevant-memories>` injection, preserving score order while truncating oversized entries and dropping overflow.
- Document the new guards in README, README_CN, and `openclaw.plugin.json`.
Removes tool exposure from the standalone `LLMRunner` when `enableTools=false`.
Some standalone text-only tasks, such as L1 extraction, expect plain text output and do not need tool access. Passing even a read-only tool subset can still encourage tool-calling behavior on OpenAI-compatible backends, which makes this path less stable than necessary.
With this change:
- `enableTools=true` keeps the existing sandboxed tool behavior
- `enableTools=false` sends no tools at all
The CMD script writes MODEL_API_KEY to .env as OPENAI_API_KEY but
does not include it in config.yaml's model section. Hermes with
provider: custom reads api_key from model.api_key in config.yaml,
not from OPENAI_API_KEY in .env. This causes 401 Authentication
Fails when users start a conversation inside the container.
Fix: add api_key field to the generated config.yaml model section.
If install_hermes_memory_tencentdb.sh is invoked directly by the root
user (e.g. fresh server with root SSH login), the prior logic:
USERNAME=$(whoami) # → "root"
if [ "$(id -u)" -eq 0 ]; then
su - $USERNAME -c "bash $TEMP_SCRIPT" # → su - root → root → loops
fi
would spin forever: ``su - root`` enters a fresh root shell that re-runs
the script, which sees EUID=0 again and ``su -``-s itself once more.
Symptom (per issue #20):
[memory-tencentdb] Running as root, switching to root for installation...
[memory-tencentdb] Running as root, switching to root for installation...
... (only Ctrl+C stops it)
Fix:
1. ``USERNAME`` resolution adds two precedence steps before
``$(whoami)`` so admins running ``sudo bash install.sh`` end up
installing for the original user instead of for root:
a. ``INSTALL_AS_USER`` env override (explicit admin choice)
b. ``SUDO_USER`` (sudo's own record of the calling user)
c. ``whoami`` (final fallback)
2. The ``id -u == 0`` branch now skips the ``su -`` step when the
target user is also root — that's the recursion-trigger case. The
script proceeds inline as root for the rest of the install.
3. A new ``elif`` arm logs a clear ``"Running as root; target user is
also root — installing in place."`` so the operator sees what's
happening.
Verified by dry-run simulation of 5 scenarios:
| Case | Branch | USERNAME |
| --------------------------------------- | ------------- | -------- |
| root SSH direct (#20 reproducer) | INLINE | root |
| non-root user direct | NORMAL | <user> |
| sudo bash install.sh from non-root user | SU | <user> |
| root + INSTALL_AS_USER=bar | SU | bar |
| root + INSTALL_AS_USER=root | INLINE | root |
Closes#20.
Signed-off-by: 李冠辰 <liguanchen@xiaomi.com>
UNSAFE_CHAR_RE included the surrogate range [\uD800-\uDFFF] without the `u`
flag, so JS treated strings as UTF-16 code units and stripped each half of
every well-formed non-BMP code point. sanitizeText and sanitizeJsonLine
therefore destroyed emoji, CJK Extension B, math bold, etc. in tool params,
tool results, and ref-md archives.
Adding the `u` flag makes paired surrogates combine into a single code point
before matching, so the [\uD800-\uDFFF] entry now matches only lone (malformed)
surrogates, which is the original intent. All other entries (replacement char,
C0/C1 controls, zero-width chars, line separators, BOM) keep their behavior.
Added a vitest suite covering the preserved and stripped cases.
Closes#30