Skip to content

feat: OpenRouter provider routing and reasoning control - #1958

Merged
shinetata merged 7 commits into
MemTensor:dev-v2.0.25from
chiefmojo:feat/openrouter-routing
Jul 22, 2026
Merged

feat: OpenRouter provider routing and reasoning control#1958
shinetata merged 7 commits into
MemTensor:dev-v2.0.25from
chiefmojo:feat/openrouter-routing

Conversation

@chiefmojo

@chiefmojo chiefmojo commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Problem

When using OpenRouter as the LLM provider, operators have no way to:

  1. Exclude specific upstream providers from serving requests (provider.ignore)
  2. Set a preferred provider order (provider.order)
  3. Control reasoning effort per model slot (reasoning)

All three are standard OpenRouter-specific request fields that MemOS currently passes through to the HTTP body without any config surface.

Changes

Config schema — adds providerIgnore, providerOrder, and reasoning to LlmSchema and SkillEvolverSchema:

llm:
  provider: openai_compatible
  endpoint: https://openrouter.ai/api/v1
  apiKey: sk-...
  providerIgnore:
    - together
    - deepinfra
  providerOrder:
    - anthropic
    - google
  reasoning:
    enabled: true
    max_tokens: 8000

SkillEvolverSchema:
  provider: openai_compatible
  endpoint: https://openrouter.ai/api/v1
  providerIgnore:
    - novita

Provider layeropenai.ts detects OpenRouter endpoints and injects the provider routing object; reasoning is forwarded unconditionally when set. Non-OpenRouter endpoints are unaffected.

EmbeddingproviderIgnore/providerOrder added to the embedding config schema; openai.ts embedding provider applies the same routing logic.

PipelinebootstrapMemoryCore forwards the new fields to createLlmClient for the skillEvolver slot.

Tests

  • providers.test.ts — reasoning forwarding, routing injection for non/streaming calls, non-OpenRouter passthrough
  • bootstrap-llm-config.test.ts — verifies routing fields propagate to dedicated LLM clients
  • load.test.ts — defaults to empty arrays, accepts explicit values

@Memtensor-AI
Memtensor-AI changed the base branch from dev-20260604-v2.0.19 to dev-v2.0.22 July 1, 2026 13:15
@Memtensor-AI

Copy link
Copy Markdown
Collaborator

Automated Test Results: PASSED

Cloud test-engine rerun against dev-v2.0.22 completed successfully.

  • Run: tr-013a21fe-6ec on cloud test-engine 10011
  • memos_local_openclaw/unit: 10 passed, 0 failed, 0 skipped
  • memos_local_plugin/unit: 90 passed, 0 failed, 0 skipped

Manual code review is still required before merge.

@Memtensor-AI

Copy link
Copy Markdown
Collaborator

Conflict status update: I verified locally that refs/pull/1958/head cleanly merges with dev-v2.0.22, and the previous cloud test-engine run passed (tr-013a21fe-6ec, 100/100). However, pushing the merge commit back to the PR head failed because the head fork repository is not accessible (Repository not found).

Current conclusion: automated tests passed, but the PR cannot be merged while GitHub still reports it as conflicting. The branch needs to be updated by the author/maintainer, or a replacement branch/PR needs to be created from the locally clean merge commit and then retested.

@CarltonXiang
CarltonXiang deleted the branch MemTensor:dev-v2.0.25 July 3, 2026 07:25
@syzsunshine219 syzsunshine219 reopened this Jul 3, 2026
@syzsunshine219
syzsunshine219 changed the base branch from dev-v2.0.22 to main July 3, 2026 08:21
@Memtensor-AI Memtensor-AI added the area:plugin OpenClaw & Hermes label Jul 8, 2026
@Memtensor-AI
Memtensor-AI requested a review from bittergreen July 8, 2026 11:43
@Memtensor-AI Memtensor-AI added the area:model llm + embedder + reranker label Jul 9, 2026
@Memtensor-AI Memtensor-AI removed the area:model llm + embedder + reranker label Jul 9, 2026
@syzsunshine219
syzsunshine219 changed the base branch from main to dev-v2.0.23 July 9, 2026 06:23
@syzsunshine219
syzsunshine219 changed the base branch from dev-v2.0.23 to main July 9, 2026 06:32

@shinetata shinetata left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for this PR, Erick — OpenRouter provider routing is a useful gap to close, and the test coverage (streaming / non-streaming / embedding, non-OpenRouter passthrough, config defaults, bootstrap forwarding) is solid. The Cloud test-engine rerun also passed (100/100) on dev-v2.0.22.

Before we can merge, a few items need attention. Marking as changes_requested to track them.

Blocking — merge conflict

GitHub currently reports this PR as CONFLICTING / DIRTY against main. The head last merged main on 2026-07-04; main has since advanced to 13fbd437 (2026-07-09). The earlier bot note referenced dev-v2.0.22, but that branch no longer exists, so a fresh rebase onto the current main is needed.

Could you rebase feat/openrouter-routing onto main and force-push? If you'd prefer a maintainer to take it over, just say so and we'll open a replacement PR (we've done this for a couple of other stale external PRs).

Code — three improvements

1. DRY: the routing helper is duplicated 4×

applyOpenRouterProviderRouting is implemented nearly verbatim in:

  • apps/memos-local-plugin/core/llm/providers/openai.ts
  • apps/memos-local-plugin/core/embedding/providers/openai.ts
  • apps/memos-local-openclaw/src/shared/llm-call.ts
  • apps/memos-local-openclaw/src/ingest/providers/openai.ts (here it's still inlined in buildRequestBody, not even extracted)

Please factor it into a single shared helper per app (the two apps/ don't share code, so one helper under each app's shared layer is fine) and have all call sites use it. The inlined version in ingest/providers/openai.ts should call the helper for consistency.

2. OpenRouter detection is brittle

endpoint.includes("openrouter.ai") is case-sensitive and substring-based:

  • OpenRouter.AI / OPENROUTER.ai won't match.
  • A self-hosted proxy whose URL happens to contain openrouter.ai as a substring (path / query / hostname fragment) would be misclassified.
  • Conversely, users fronting OpenRouter via their own domain (reverse proxy / CNAME) get no routing even though the upstream is OpenRouter.

Suggest normalizing with new URL(endpoint).hostname lowercased and comparing against a known host list, and/or adding an explicit openrouter: true opt-in flag on the LLM / embedding config so routing isn't inferred from the URL at all.

3. reasoning is forwarded unconditionally

In core/llm/providers/openai.ts:

if (config.reasoning) body.reasoning = config.reasoning;

This sends reasoning to every endpoint, not just OpenRouter. Non-OpenRouter providers may reject the unknown field or silently ignore it. Either gate it behind the OpenRouter check, or document explicitly that it's a passthrough intended for OpenAI-compatible reasoning models and accept that behavior.

Deadline

Could you push an update by 2026-07-23? If there's no activity by then, to keep main moving we'll close this PR and open a maintainer-side replacement with the above items addressed (credit will be preserved in the replacement PR description). Happy to help with the rebase or the refactor if useful — just ping here.

@chiefmojo
chiefmojo force-pushed the feat/openrouter-routing branch from 40af2fd to 2c8178d Compare July 17, 2026 03:56
@Memtensor-AI Memtensor-AI added the status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 label Jul 17, 2026
@Memtensor-AI
Memtensor-AI requested review from hijzy and whipser030 July 17, 2026 03:56
@Memtensor-AI

Memtensor-AI commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

🤖 Open Code Review

Target: PR #1958
Task: e587adf61a6135f5
Base: dev-v2.0.25
Head: feat/openrouter-routing

🔍 OpenCodeReview found 7 issue(s) in this PR.

⚠️ 1 warning(s) occurred during review.


1. apps/memos-local-plugin/core/embedding/types.ts (L34-L35)

Type contract mismatch: openRouter is declared as a required boolean in EmbeddingConfig, but in EmbeddingSchema it is defined as Type.Optional(Bool(false)), which TypeBox resolves to boolean | undefined. If any caller constructs or passes an EmbeddingConfig without going through the full config resolution pipeline (e.g., in unit tests or partial config objects), openRouter may be undefined at runtime even though the type says it cannot be. To stay consistent with the schema, mark it optional here too (openRouter?: boolean), or ensure the schema uses a non-optional field. Compare: providerIgnore and providerOrder are correctly marked ? here.

💡 Suggested Change

Before:

  /** Explicitly enable OpenRouter fields for a reverse proxy or CNAME. */
  openRouter: boolean;

After:

  /** Explicitly enable OpenRouter fields for a reverse proxy or CNAME. */
  openRouter?: boolean;

2. apps/memos-local-plugin/core/llm/providers/openai.ts (L217-L223)

The effort field allows the literal value "none" (defined in ReasoningSchema), which is not a valid value in the OpenRouter reasoning API. OpenRouter's reasoning object accepts "low", "medium", and "high" for effort, and disabling reasoning is done via { enabled: false } rather than { effort: "none" }. Sending effort: "none" will likely cause a 4xx validation error from OpenRouter.

Suggestion: Remove "none" from the effort union in ReasoningSchema, or add a guard here to skip serializing effort when its value is "none" and map it to enabled: false instead.

💡 Suggested Change

Before:

function serializeOpenRouterReasoning(reasoning: ReasoningConfig): Record<string, unknown> | undefined {
  const result: Record<string, unknown> = {};
  if (reasoning.enabled !== undefined) result.enabled = reasoning.enabled;
  if (reasoning.effort !== undefined) result.effort = reasoning.effort;
  if (reasoning.maxTokens !== undefined) result.max_tokens = reasoning.maxTokens;
  return Object.keys(result).length > 0 ? result : undefined;
}

After:

function serializeOpenRouterReasoning(reasoning: ReasoningConfig): Record<string, unknown> | undefined {
  const result: Record<string, unknown> = {};
  if (reasoning.enabled !== undefined) result.enabled = reasoning.enabled;
  if (reasoning.effort !== undefined && reasoning.effort !== "none") result.effort = reasoning.effort;
  if (reasoning.maxTokens !== undefined) result.max_tokens = reasoning.maxTokens;
  return Object.keys(result).length > 0 ? result : undefined;
}

3. apps/memos-local-plugin/core/llm/providers/openai.ts (L78-L81)

Because config.reasoning has a schema default of {} (an empty object), it will always be truthy after config resolution — meaning serializeOpenRouterReasoning is always called when OpenRouter is detected, even when no reasoning fields were configured. The config.reasoning && guard is misleading and provides no actual protection. This is harmless only because serializeOpenRouterReasoning returns undefined for an empty object, but the guard creates false confidence about a potential early-exit. Consider removing the guard or adding a comment clarifying this.

💡 Suggested Change

Before:

    if (applyOpenRouterProviderRouting(config, body)) {
      const reasoning = config.reasoning && serializeOpenRouterReasoning(config.reasoning);
      if (reasoning) body.reasoning = reasoning;
    }

After:

    if (applyOpenRouterProviderRouting(config, body)) {
      const reasoning = serializeOpenRouterReasoning(config.reasoning ?? {});
      if (reasoning) body.reasoning = reasoning;
    }

4. apps/memos-local-plugin/core/config/schema.ts (L61-L69)

The effort union includes "minimal", "xhigh", and "max" which are not valid values in the OpenRouter API spec. The OpenRouter reasoning object's effort field only accepts "low", "medium", and "high". Sending an unrecognised value will either be silently ignored or cause a 4xx error from OpenRouter, with no validation feedback to the user.

Consider restricting the union to the documented values, or — if you want to stay forward-compatible with new provider-specific values — replace the closed union with a plain Type.String() and add a comment referencing the API docs.

💡 Suggested Change

Before:

  effort: Type.Optional(Type.Union([
    Type.Literal("minimal"),
    Type.Literal("none"),
    Type.Literal("low"),
    Type.Literal("medium"),
    Type.Literal("high"),
    Type.Literal("xhigh"),
    Type.Literal("max"),
  ])),

After:

  // Valid values per OpenRouter API docs: "low" | "medium" | "high"
  effort: Type.Optional(Type.Union([
    Type.Literal("low"),
    Type.Literal("medium"),
    Type.Literal("high"),
  ])),

5. apps/memos-local-plugin/core/config/schema.ts (L52)

ReasoningSchema is declared with { default: {} } at the object level, but it is only ever used inside Type.Optional(ReasoningSchema). When TypeBox resolves the config it will materialise the default {} for any omitted reasoning block, meaning config.reasoning will be {} rather than undefined. The jsdoc comment says "Omit the whole block to keep the provider/model default", which contradicts this behaviour.

Drop the object-level default so that an absent reasoning key correctly resolves to undefined, consistent with the documented intent.

💡 Suggested Change

Before:

}, { default: {} });

After:

});

6. apps/memos-local-plugin/core/llm/types.ts (L39-L40)

Type inconsistency: openRouter is required in LlmConfig but optional in OpenRouterRoutingConfig.

In openrouter.ts, OpenRouterRoutingConfig declares openRouter?: boolean (optional). The config schema also defines it as Type.Optional(Bool(false)). However, LlmConfig.openRouter is a required non-optional boolean, creating a structural mismatch between the two interfaces that represent the same concept.

This means any code path that builds a partial LlmConfig (e.g., before defaults are applied) or passes a value satisfying OpenRouterRoutingConfig will either fail type-checking or require explicit assertion. The field should be made optional (openRouter?: boolean) to be consistent with its schema definition and with OpenRouterRoutingConfig, especially since applyOpenRouterProviderRouting accepts OpenRouterRoutingConfig and the runtime already guards with the isOpenRouter check that handles undefined.

💡 Suggested Change

Before:

  /** Explicitly enable OpenRouter fields for a reverse proxy or CNAME. */
  openRouter: boolean;

After:

  /** Explicitly enable OpenRouter fields for a reverse proxy or CNAME. */
  openRouter?: boolean;

7. apps/memos-local-plugin/core/pipeline/memory-core.ts (L124-L135)

DedicatedLlmConfig is a private type re-declaration that closely mirrors the static type derived from SkillEvolverSchema (which config.skillEvolver and config.l3Llm already conform to). The type-cast (config as { skillEvolver?: DedicatedLlmConfig }) is therefore redundant. Since config is a ResolvedConfig, its skillEvolver and l3Llm fields are already fully typed. Accessing them directly (e.g., config.skillEvolver) avoids the risk of this local type silently drifting out of sync with the schema.

Suggestion: Remove DedicatedLlmConfig and access the config fields directly as config.skillEvolver and config.l3Llm.


🧹 Filtered 13 low-confidence OCR finding(s) before posting/fix-loop (existing_code_mismatch: 5, duplicate: 8).

Generated by cloud-assistant via Open Code Review.

@chiefmojo

Copy link
Copy Markdown
Contributor Author

Rebased onto current main and force-pushed 2c8178d. Consolidated OpenRouter routing into one helper per app, replaced all four call sites, added robust hostname detection plus explicit openRouter proxy opt-in, and gated/normalized reasoning fields. Also fixed dedicated L3 config propagation. Validation: 66 focused plugin tests, 3 OpenClaw call tests, plugin type-check, and make format.

@chiefmojo
chiefmojo requested a review from shinetata July 17, 2026 04:12
@Memtensor-AI

Copy link
Copy Markdown
Collaborator

✅ Automated Test Results: PASSED

All tests passed (134/134 executed). memos_local_openclaw/unit: 33/33, memos_local_plugin/unit: 101/101. Duration: 17s [advisory, non-gating] AI-generated tests on branch test/auto-gen-650698fd619b8f1a-20260717122622: 110/112 passed, 2 failed — these do NOT affect the PR verdict; review the branch manually.

Branch: feat/openrouter-routing

@Memtensor-AI Memtensor-AI added status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发 and removed status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 labels Jul 17, 2026
@whipser030

Copy link
Copy Markdown
Collaborator

Thanks for the update — the earlier concerns around OpenRouter routing detection, reasoning passthrough for non-OpenRouter requests, and helper reuse appear to be addressed well, and the test coverage looks solid.

Before merging, could we please clarify two small edge cases?

  1. reasoning: {} may currently enable reasoning through the default value of enabled. This seems slightly at odds with the documentation stating that omitted configuration should preserve the provider/model default. Would it make sense to avoid enabling it by default here?

  2. In a few dedicated LLM configuration paths, openRouter may be passed as undefined while its type is declared as a required boolean. It may be worth adding ?? false at the handoff point, or making the field optional consistently, to keep the runtime behavior and type contract aligned.

Also, when convenient, could you please confirm that the latest branch now merges cleanly with current main? Once that is confirmed, I’d be happy to take another look.

@Memtensor-AI Memtensor-AI added status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 and removed status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发 labels Jul 17, 2026
@chiefmojo

Copy link
Copy Markdown
Contributor Author

@whipser030 Addressed in aa75c36:

  • Empty reasoning blocks no longer default enabled to true and are omitted from OpenRouter request bodies, preserving provider/model defaults.
  • Dedicated skillEvolver and l3Llm clients now normalize an absent openRouter flag to false.

Added focused regressions for both paths. The branch contains current main and GitHub reports the PR mergeable.

@Memtensor-AI

Copy link
Copy Markdown
Collaborator

✅ Automated Test Results: PASSED

All tests passed (137/137 executed). memos_local_openclaw/unit: 33/33, memos_local_plugin/unit: 104/104. Duration: 17s [advisory, non-gating] AI-generated tests on branch test/auto-gen-b5be9075636df774-20260717203542: 126/126 passed — these do NOT affect the PR verdict; review the branch manually.

Branch: feat/openrouter-routing

@Memtensor-AI Memtensor-AI added status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发 and removed status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 labels Jul 17, 2026

@shinetata shinetata left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@chiefmojo Thanks for the fast turnaround. I re-reviewed the current state (through aa75c369) and all the points from both review rounds are addressed:

  • ✅ Routing helper consolidated into one shared module per app; all four call sites use it
  • ✅ OpenRouter detection now uses new URL(...).hostname against a host allowlist + explicit openRouter opt-in
  • reasoning is only forwarded on OpenRouter endpoints, and empty reasoning blocks are omitted (no implicit enabled: true)
  • skillEvolver / l3Llm normalize a missing openRouter flag to false
  • ✅ CI green across the board (build matrix + AutoTest); the OCR findings are non-blocking

Approving now. The only remaining step before merge is that the branch is 2 commits behind main — it's a clean update (no conflicts, MERGEABLE); branch protection just needs the head to be up to date. Since this is a fork PR we can't reach with maintainer edits, could you sync it on your side?

git checkout feat/openrouter-routing
git fetch upstream                 # upstream = MemTensor/MemOS
git rebase upstream/main           # only 2 commits, no conflicts expected
git push --force-with-lease origin feat/openrouter-routing

(A plain git merge upstream/main + push is fine too if you'd rather not rewrite history.)

Once it's up to date it's good to squash-merge — well before the 07-23 date, so no need to worry about the replacement-PR route. The 13 OpenCodeReview nits (doc comments, optional-type alignment) are non-blocking and can be a follow-up. Thanks again for the thorough work!

@shinetata
shinetata changed the base branch from main to dev-v2.0.25 July 21, 2026 12:58
@shinetata

Copy link
Copy Markdown
Collaborator

Quick correction to my earlier note — no branch sync is needed on your side. Retargeting external PRs onto our weekly release branch (dev-v2.0.25) is a maintainer-side step; since that branch does not require the head to be up to date and there are no conflicts, I have switched the base and am merging from here. Nothing further needed from you — thanks again for the thorough OpenRouter work.

@Memtensor-AI Memtensor-AI added status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 and removed status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发 labels Jul 21, 2026
@Memtensor-AI

Copy link
Copy Markdown
Collaborator

✅ Automated Test Results: PASSED

All tests passed (137/137 executed). memos_local_openclaw/unit: 33/33, memos_local_plugin/unit: 104/104. Duration: 17s [advisory, non-gating] AI-generated tests on branch test/auto-gen-e587adf61a6135f5-20260721223302: 74/78 passed, 4 failed — these do NOT affect the PR verdict; review the branch manually.

Branch: feat/openrouter-routing

@Memtensor-AI Memtensor-AI added status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发 and removed status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 labels Jul 21, 2026
@shinetata
shinetata merged commit afd9d31 into MemTensor:dev-v2.0.25 Jul 22, 2026
18 checks passed
CarltonXiang added a commit that referenced this pull request Jul 24, 2026
* ci: automate OpenClaw local plugin release notes (#2129)

Generate evidence-backed OpenClaw local plugin release notes through the configured draft service.

Add dry-run and publish workflow safeguards, retry/repair validation, prerelease handling, failure reporting hooks, redacted inspection artifacts, and focused local plugin test coverage.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix: Adjust feedback update review flow & using independent

* refactor: unify LLM provider environment config

# Conflicts:
#	examples/basic_modules/llm.py
#	src/memos/mem_reader/multi_modal_struct.py

* fix: fix log for embedding models

* feat(api):update SDK version number

* fix: fix tests

* fix: preserve text when downgrading feedback updates

* feat(api):update SDK version number

* fix: type parser factory config as base parser config

* fix: Open Code Review problems fix

* feat: OpenRouter provider routing and reasoning control (#1958)

* feat(memos): add OpenRouter provider routing

* fix(memos): route OpenRouter prefs through all slots

* Fix OpenRouter reasoning config types

* fix(openrouter): harden routing detection

* fix(openrouter): harden review follow-ups

* fix(openrouter): preserve provider defaults

---------

Co-authored-by: jiachengzhen <jiacz@memtensor.cn>
Co-authored-by: shinetata <149466187+shinetata@users.noreply.github.com>

* refactor: summarize search and embedding logs

* perf: cache and coalesce embedding requests

* fix: summarize cosine reranker logs

* perf: optimize PolarDB retrieval round trips

* perf: prune MMR candidates by memory bucket

* Fix #1611: classifyTopic fails when summarizer uses Anthropic endpoint (Kimi Code) (#2133)

* fix(openclaw): route classifyTopic + arbitrateTopicSplit per provider (#1611)

`Summarizer.classifyTopic` and `Summarizer.arbitrateTopicSplit` always
dispatched to the OpenAI implementation regardless of the summarizer's
configured provider. When configured against an Anthropic-only endpoint
(e.g. Kimi Code's `/coding/v1/messages`), every call returned 404 because
the OpenAI helper appended `/chat/completions` to a URL that does not
exist — wasting tokens, polluting logs, adding event-loop latency.

Fix (Option A from the issue):

- Add native classifyTopic{Anthropic,Gemini,Bedrock} and
  arbitrateTopicSplit{Anthropic,Gemini,Bedrock} in the three provider
  files, mirroring the shape of the existing judgeNewTopic* helpers.
- Re-export TOPIC_CLASSIFIER_PROMPT / TOPIC_ARBITRATION_PROMPT from
  openai.ts so all providers share prompt strings and parseTopicClassifyResult
  remains the single JSON parser — no behavioral drift across providers.
- Rewire callTopicClassifier / callTopicArbitration switch statements
  so the anthropic / gemini / bedrock arms route to their native
  implementations instead of falling through to the OpenAI helpers.
- Bedrock helpers require cfg.endpoint (throws
  `Bedrock topic-classifier|arbitration requires 'endpoint'`) matching
  every other Bedrock helper in the file.
- Mirror every edit into packages/memos-core/src/ingest/providers/ so
  the byte-similar sibling tree cannot silently regress the bug after
  the next sync.

Tests: new tests/topic-classifier-dispatch.test.ts (8 cases) stubs
globalThis.fetch and asserts each provider hits the correct URL + body
shape; includes a regression case where an Anthropic 404 surfaces an
`Anthropic topic-classifier failed` error rather than the old
`OpenAI topic-classifier failed` — the exact string from issue #1611.
TDD baseline: 7/8 fail against buggy code. Post-fix: 8/8 pass. Existing
tests/topic-judge-minimax-1315.test.ts (5/5) continues to pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(openclaw): harden topic-classifier/arbitrator providers per OCR review

Address code-review findings on the topic-classifier + arbitrator dispatch
helpers added in #2133 (issue #1611). Applied to both
apps/memos-local-openclaw and packages/memos-core (byte-similar mirrors).

Correctness / security fixes:
- anthropic: validate cfg.apiKey up front instead of silently sending "";
  move cfg.headers spread BEFORE x-api-key + anthropic-version so a
  misconfigured header cannot silently overwrite the credential
- anthropic: guard content parsing — treat missing/non-array json.content
  as [] to avoid TypeError on unexpected API payloads
- anthropic: reduce arbitrateTopicSplit max_tokens from 60 to 10 to match
  arbitrateTopicSplitOpenAI (single-word NEW/SAME reply)
- gemini: validate cfg.apiKey up front; construct URL via new URL() +
  URLSearchParams so a caller-supplied endpoint that already contains a
  query string ("?version=v1beta") does not produce malformed
  "…?version=v1beta?key=…"
- bedrock: emit warn log when arbitrateTopicSplit sees empty or
  unexpected text so silent bias toward SAME becomes visible at normal
  log levels (production usually suppresses debug)

Intentionally not applied (out-of-scope refactors that would touch
pre-existing helpers not in this PR): DRY-extract shared low-level
callers for anthropic/gemini/bedrock (findings #4/#7/#10/#14) and the
DEFAULT_BEDROCK_MODEL constant (finding #11).

Existing tests unchanged: tests/topic-classifier-dispatch.test.ts (8/8)
and tests/topic-judge-minimax-1315.test.ts (5/5) still pass. The new
guards use apiKey values already provided by every test case, so no
test edits were required.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(openclaw): address remaining OCR round-2 findings on topic-classifier providers

Round 1 (commit 646c28a) did not fully converge OCR review. Round 2 targets
the 2 repeated findings and 10 newly detected ones without expanding scope
beyond the classifier/arbitration functions added by PR #2133.

Bedrock (packages/memos-core + apps/memos-local-openclaw):
- Fix TRUE MISS from Round 1: `arbitrateTopicSplitBedrock` was still using
  `maxTokens: 60` while Anthropic's arbitrator was already lowered to 10.
  Aligned to `maxTokens: 10`.
- Introduce `DEFAULT_BEDROCK_TOPIC_MODEL` constant scoped to the two new
  functions only (`classifyTopicBedrock`, `arbitrateTopicSplitBedrock`).
  Pre-existing helpers (`judgeDedupBedrock`, `summarizeBedrock`, etc.) keep
  their inline default to stay out of scope.
- Extract `bedrockConverseTopic()` shared transport used by the two new
  functions to eliminate the DRY duplication OCR flagged.

Gemini (packages/memos-core + apps/memos-local-openclaw):
- Add empty/unexpected-response `log.warn` branches to
  `arbitrateTopicSplitGemini`, mirroring the Bedrock arbitrator's
  defensive handling (Round 1 covered Bedrock only).
- Extract `callGeminiTopic()` shared transport used by the two new
  functions to eliminate the DRY duplication OCR flagged.
- Rejected escalating the empty-response case to `throw`: throwing would
  cascade through `Summarizer.tryChain` and burn tokens on every fallback;
  warn+default-to-SAME is the safer choice for a boundary detector.

Anthropic (packages/memos-core + apps/memos-local-openclaw):
- Extract `callAnthropicMessagesTopic()` shared transport used by the two
  new functions to eliminate the DRY duplication OCR flagged. Header spread
  order is preserved verbatim: `...cfg.headers` comes BEFORE the credential
  and `anthropic-version` so user-supplied headers cannot override them
  (this was the Round 1 security fix — do not undo).

Pre-existing helpers in each provider are intentionally untouched to keep
the diff minimum-scoped to PR #2133's classifier/arbitrator additions.

Verification (executed in apps/memos-local-openclaw):
- `./node_modules/.bin/vitest run tests/topic-classifier-dispatch.test.ts \\
  tests/topic-judge-minimax-1315.test.ts` → 13/13 passed
- `./node_modules/.bin/tsc --noEmit` on the 3 modified provider files → 0 errors
- All 6 files (3 apps + 3 packages/memos-core mirrors) are byte-identical

---------

Co-authored-by: MemOS AutoDev <autodev@memtensor.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix #2131: [Bug] memos-local-plugin : chunks table never created — causes viewer dashboard  (#2132)

* fix: viewer dashboard drifts to zero after a namespace flip (#2131)

The viewer's Overview/metrics/traces/session routes filtered reads
through the core's mutable activeNamespace, which every turn/session
rewrites from caller context hints (openclaw: profileId = ctx.agentId).
When the gateway session or a sub-agent turn flipped the profile, all
historical rows failed the visibility clauses and dashboard counts
collapsed to zero.

The reported root cause (missing chunks table) is a misdiagnosis: the
only 2.0 reference to chunks is the legacy 1.0-import reader in
server/routes/migrate.ts; the live pipeline queries traces/episodes/
sessions with vectors stored in BLOB columns, so no schema change is
needed.

Fix: complete the existing includeAllNamespaces viewer convention
(already used by diag.ts/session.ts/memory.ts) — add the option to
core.metrics() and core.listEpisodes(), and pass it from overview.ts,
metrics.ts, trace.ts and session.ts. Scoped reads and turn-time
retrieval isolation are unchanged; explicit owner filters still narrow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: align listApiLogs namespace scoping with viewer metrics feeds

Follow-up to #2131 (OCR round 1/2):

- listApiLogs: accept includeAllNamespaces in the contract and core
  facade (contract symmetry with listTraces/listSkills/listPolicies);
  metrics/tools now passes it explicitly so both feeds of the bump()
  aggregation are scoped identically
- metrics(): document that the traces fetch feeding sessions/
  writesToday/embeddings/dailyWrites is intentionally cross-namespace;
  only totalTurns respects includeAllNamespaces
- listEpisodes: apply limit/offset after the visibility filter on the
  namespace-scoped path so pages are not silently under-filled by
  other namespaces' rows

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: memos-autodev[bot] <autodev-bot@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* fix(plugin): paginate dirty-closed reward recovery (#2120)

* fix(plugin): page dirty-closed reward recovery

* fix(plugin): share dirty scan cursor type

---------

Co-authored-by: HarveyXiang <harvey_xiang@163.com>

* fix(adapter): fire session.close in daemon thread to unblock event loop (#1953)

* fix(adapter): fire session.close in daemon thread to unblock event loop

on_session_end() called bridge.request("session.close") inline with a
30 s blocking urlopen(). gateway/run.py calls this synchronously from
_handle_reset_command (an async fn), so the blocking I/O ran on the
asyncio event loop thread, preventing Discord heartbeats from firing and
causing forced reconnection after 10–30 s.

The session.close response is unused and errors are already suppressed,
so the call is semantically fire-and-forget. Moving it to a daemon thread
is the correct fix: the event loop is never blocked, the request still
goes out, and a 5 s timeout keeps it bounded if the bridge is dead.

Reproducer: Violet 2026-06-12 09:54 (agent.log lines 3629–3791).
Spec: ~/specs/memos-bridge-blocking-shutdown-spec.md

* Fix #1897: bug(memos-local-plugin): Hermes background model status checks can trigger paid  (#1899)

* Fix #1897: fix(memos-local-plugin): add LLM circuit breaker for terminal provider errors

Issue #1897 reported ~12,900 paid LLM requests in 24 h on Hermes
against a DeepSeek key with insufficient balance. The local
`system_model_status` row count (12,900) closely tracked the
provider-side `request_count` (11,344) for the same billing window.
The naming is misleading: `system_model_status` is not a health probe;
it is the audit row written once per LLM call (ok / fallback / error)
inside `core/llm/client.ts`. With no circuit breaker, every pipeline
subscriber (capture / session-relation / reward / L2 / L3 / skill /
retrieval LLM filter / world-model) kept firing on every turn / closed
episode / induction, generating one paid request each.

Add a per-`LlmClient` circuit breaker:

- Trips on terminal errors: HTTP 401/402/403 or messages containing
  `insufficient balance` / `invalid api key` / `unauthorized` /
  `account suspended` / `billing`.
- Open: short-circuits subsequent calls inside the facade without
  contacting the provider. Throws `MemosError(LLM_UNAVAILABLE)` with
  `details.circuitOpen=true` so existing catch blocks still work.
- Half-open after cool-down (default 5 min, configurable, min 30 s):
  next call probes the provider; success closes the breaker, terminal
  failure re-opens it for another cool-down.
- Host fallback rescues a call without tripping the breaker —
  fallback exists precisely to keep going when the primary is down.
- Coalesces `system_model_status="circuit_open"` audit rows to at
  most one per ~25 s while the breaker stays open, so we don't
  replace paid spam with audit-row spam.
- Exposes `circuitOpen` / `circuitOpenUntil` / `circuitOpenedReason`
  via `LlmClientStats` for the Overview viewer card.
- Enabled by default; legacy behaviour available via
  `circuitBreaker.enabled = false`.

Tests: 9 new vitest cases under `tests/unit/llm/client.test.ts`
covering trip on 402, trip on "insufficient balance" message, no
trip on generic transient, coalescing, half-open close on success,
host-fallback rescues without trip, disabled mode, stats fields,
and re-open on terminal probe failure. All 59 LLM and 28 pipeline
tests pass; `tsc --noEmit` clean.

Out of scope (tracked separately): 429 `Retry-After` handling
(issue #1620), per-tool rate limits, daily budget caps.

* Fix LLM breaker with host fallback

---------

Co-authored-by: autodev-bot <autodev@memtensor.local>
Co-authored-by: Jiang <33757498+hijzy@users.noreply.github.com>
Co-authored-by: GU TIANCHUN <96930846+TianchunGu@users.noreply.github.com>
Co-authored-by: Dubberman <48425266+whipser030@users.noreply.github.com>

---------

Co-authored-by: Memtensor-AI <project@memtensor.cn>
Co-authored-by: autodev-bot <autodev@memtensor.local>
Co-authored-by: Jiang <33757498+hijzy@users.noreply.github.com>
Co-authored-by: GU TIANCHUN <96930846+TianchunGu@users.noreply.github.com>
Co-authored-by: Dubberman <48425266+whipser030@users.noreply.github.com>

* feat: configurable log timezone (#1956)

feat(2c): add log timezone support

Co-authored-by: Fayenix <fayenix@users.noreply.github.com>
Co-authored-by: jiachengzhen <jiacz@memtensor.cn>

* feat: chunk batch reflection scoring (#2146)

Co-authored-by: Erick Wipprecht <53452309+chiefmojo@users.noreply.github.com>

* Fix #1897 recovery replay request storm (#1939)

* Fix #1897: fix(memos-local-plugin): add LLM circuit breaker for terminal provider errors

Issue #1897 reported ~12,900 paid LLM requests in 24 h on Hermes
against a DeepSeek key with insufficient balance. The local
`system_model_status` row count (12,900) closely tracked the
provider-side `request_count` (11,344) for the same billing window.
The naming is misleading: `system_model_status` is not a health probe;
it is the audit row written once per LLM call (ok / fallback / error)
inside `core/llm/client.ts`. With no circuit breaker, every pipeline
subscriber (capture / session-relation / reward / L2 / L3 / skill /
retrieval LLM filter / world-model) kept firing on every turn / closed
episode / induction, generating one paid request each.

Add a per-`LlmClient` circuit breaker:

- Trips on terminal errors: HTTP 401/402/403 or messages containing
  `insufficient balance` / `invalid api key` / `unauthorized` /
  `account suspended` / `billing`.
- Open: short-circuits subsequent calls inside the facade without
  contacting the provider. Throws `MemosError(LLM_UNAVAILABLE)` with
  `details.circuitOpen=true` so existing catch blocks still work.
- Half-open after cool-down (default 5 min, configurable, min 30 s):
  next call probes the provider; success closes the breaker, terminal
  failure re-opens it for another cool-down.
- Host fallback rescues a call without tripping the breaker —
  fallback exists precisely to keep going when the primary is down.
- Coalesces `system_model_status="circuit_open"` audit rows to at
  most one per ~25 s while the breaker stays open, so we don't
  replace paid spam with audit-row spam.
- Exposes `circuitOpen` / `circuitOpenUntil` / `circuitOpenedReason`
  via `LlmClientStats` for the Overview viewer card.
- Enabled by default; legacy behaviour available via
  `circuitBreaker.enabled = false`.

Tests: 9 new vitest cases under `tests/unit/llm/client.test.ts`
covering trip on 402, trip on "insufficient balance" message, no
trip on generic transient, coalescing, half-open close on success,
host-fallback rescues without trip, disabled mode, stats fields,
and re-open on terminal probe failure. All 59 LLM and 28 pipeline
tests pass; `tsc --noEmit` clean.

Out of scope (tracked separately): 429 `Retry-After` handling
(issue #1620), per-tool rate limits, daily budget caps.

* Fix LLM breaker with host fallback

* fix(plugin): bound recovery reflect replay LLM calls

---------

Co-authored-by: autodev-bot <autodev@memtensor.local>
Co-authored-by: Jiang <33757498+hijzy@users.noreply.github.com>
Co-authored-by: jiachengzhen <jiacz@memtensor.cn>

* fix: guard against episode storm stalling foreground sessions (#1844)

fix: guard against episode storm stalling foreground sessions (#1755)

Co-authored-by: jiachengzhen <jiacz@memtensor.cn>

* fix(logging): initialize logger from config in the standalone bridge (#1955)

The MemOS daemon (`bridge.cts`) resolved config inside
`bootstrapMemoryCoreFull` but never called `initLogger`, so the active
logger stayed on the `bootstrapConsoleOnly()` default with `tz` pinned to
"UTC". This made `logging.timezone` (PR #44) inert in the daemon — and the
rest of `logging.*` (level, channels, file/audit/llm/perf/events sinks)
dead too. Pretty/compact timestamps kept printing UTC regardless of config.

Add an opt-in `initLogging` flag to `BootstrapOptions`. When set,
`bootstrapMemoryCoreFull` calls `initLogger(config, home)` right after
config resolution, before anything logs. The standalone bridge passes it;
embedded plugin hosts (`adapters/openclaw/index.ts`) leave it off so the
host keeps control of its own logging.

Test: bootstrap with `logging.timezone` set + `initLogging: true` stamps
records with the configured tz; without the flag the logger is untouched.

* fix(bridge): add 20s timeout guard to core.shutdown() to prevent orphaned processes (#1799)

* fix(bridge): add shutdown timeout to prevent orphan bridge processes

core.shutdown() drains the L2/L3/skill flush pipeline which can block
on hanging LLM calls. Without a deadline the bridge never exits after
stdin EOF when the Python parent is already gone, re-creating the
process-leak condition. Race all three shutdown sites (daemon SIGTERM,
non-daemon SIGTERM, headless stdin-EOF) against a 20s timeout so the
process always terminates within a bounded time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(bridge): wrap remaining core.shutdown() calls with 20s timeout

Three sites missed by the original patch (56ebe7a3) were calling
core.shutdown() without withShutdownTimeout, leaving the bridge process
able to hang indefinitely if L2/L3/skill LLM calls stalled at shutdown:

  • bridge.cts: EADDRINUSE exit (×2) — daemon can't bind viewer port
  • bridge.cts: viewer-running keepalive path — stdin closes but viewer
    is still serving; core.shutdown fires from the interval callback

All six core.shutdown() call sites now go through withShutdownTimeout,
guaranteeing the bridge exits within 20s regardless of which path is
taken. Adds bridge-shutdown-audit.md documenting the full call chain and
confirming no blocking sync calls prevent the timeout from firing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(docs): correct stale line references + move audit doc to apps/memos-local-plugin/docs/

- Removed companion-stable branch reference from header
- Updated bridge.cts line numbers to match main (per shinetata review)
- Removed companion-stable commit hash 56ebe7a3
- Moved from repo root to apps/memos-local-plugin/docs/ per review request

---------

Co-authored-by: Fayenix <fayenix@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(build): rebuild bridge.cjs when any TypeScript source is newer (#1850)

* fix(migrations): remove duplicate ALTER TABLE stmts from 008 now in 001

All columns added by 008-feedback-experience-metadata were backported into
001-initial.sql; the ALTER TABLE statements now fail on fresh DBs. Keeps
only the idempotent CREATE INDEX lines.

Also fix scripts/replay.py bridge_client import path (adapters/ is under
apps/memos-local-plugin/, not the repo root) and add
scripts/export-state-sessions.py for exporting state.db sessions to JSON
files compatible with replay.py.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(daemon): rebuild when any .ts source is newer than dist/bridge.cjs

_rebuild_if_stale() only checked bridge.cts mtime, so changes to
core/**/*.ts (e.g. memory-core.ts) didn't trigger an auto-rebuild on
gateway restart. Walk all .ts files under plugin_root and compare the
newest mtime against the compiled binary instead.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: remove 500-row cap that truncated viewer count displays (#1954)

* fix: remove 500-row cap that truncated viewer count displays

clampLimit() in _helpers.ts capped all repo list() calls at 500,
silently truncating skill/policy/world-model counts everywhere they
were derived from array length rather than a COUNT(*) query.

- Raise clampLimit cap 500 → 100,000 so metrics() and other internal
  analytics can read full datasets (fixes Analytics tab)
- Rewrite /api/v1/overview to use countSkills/countPolicies/
  countWorldModels/countEpisodes instead of list+length, making
  Overview counts correct regardless of scale (fixes Overview tab)
- Align countSkills to use limit:100_000 consistent with other
  count methods

* feat: chunk batch reflection scoring (#1957)

* Fix #1540: fix: (#1824)

docs(memos-local-plugin): clarify install path and stale dir names (#1540)

The README's 'Quick start' section told users to use install.sh instead
of npm install, but the warning was buried and users still tried
'npm install -g @memtensor/memos-local-plugin' first. The reporter in
#1540 encountered this on a Hermes deployment.

This change:

- Promotes the 'do not run npm install -g' notice to a prominent
  IMPORTANT callout explaining why global install is wrong (no
  agent-home deploy, no config.yaml, no bridge/viewer) and that the
  tarball intentionally ships built artifacts only.
- Adds a Troubleshooting subsection covering the two specific symptoms
  in the bug report: the 'package not found' misread, and the stale
  web/ and site/ directory names (web/ is now viewer/, site/ was
  removed by commit 26e7e3d).
- Mentions install.ps1 for Windows alongside install.sh.
- CHANGELOG: record the docs fix and reference #1540.

Documentation-only change; no code or runtime behavior touched.

Co-authored-by: MemOS AutoDev <autodev@memtensor.ai>
Co-authored-by: Matthew <heimixiaozhuang@zju.edu.cn>

* Fix #1888: [Bug] test_system_parser.py: SystemParser.__init__() got an unexpected keyword a (#1889)

fix: remove invalid chunker parameter from SystemParser test instantiation

- SystemParser.__init__() signature changed to (embedder, llm=None)
- Test was still passing chunker=None causing TypeError
- Fixes all 5 failing tests in test_system_parser.py

Fixes #1888

Co-authored-by: MemOS AutoDev <autodev@memos.ai>
Co-authored-by: Matthew <heimixiaozhuang@zju.edu.cn>

* Fix #1525: [Bug] clean_json_response crashes with cryptic AttributeError when given None (#1884)

* test: add comprehensive tests for clean_json_response (issue #1525)

- Add test suite in tests/mem_os/test_format_utils.py
- Cover None input ValueError with diagnostic message
- Cover markdown removal, whitespace stripping, edge cases
- Verify fix for AttributeError when LLM returns None

* style: format clean_json_response tests

---------

Co-authored-by: MemOS AutoDev <autodev@memos.ai>
Co-authored-by: Matthew <heimixiaozhuang@zju.edu.cn>

* Fix #1901: share_cube_with_user passes swapped args to _validate_cube_access — fails for ev (#1903)

fix: validate current user not target in share_cube_with_user (#1901)

share_cube_with_user(cube_id, target_user_id) called
_validate_cube_access(cube_id, target_user_id), but the validator
signature is (user_id, cube_id). The cube_id therefore landed in the
user_id slot and _validate_user_exists raised
"User '<cube_id>' does not exist or is inactive" for every well-formed
call, making the API unusable.

The in-code comment "Validate current user has access to this cube"
already documented the correct intent: the sharing user (self.user_id)
must have access to the cube being shared, not the target. Switch the
call to self._validate_cube_access(self.user_id, cube_id). The target
user's existence is independently checked on the next line via
validate_user(target_user_id), so that path is unchanged.

Add regression tests in tests/mem_os/test_memos_core.py that pin down:
- validate_user_cube_access is consulted with (self.user_id, cube_id),
- add_user_to_cube is called with (target_user_id, cube_id) on success,
- a missing target raises "Target user '<id>' does not exist".

Closes #1901

Co-authored-by: MemOS AutoDev Bot <autodev@memtensor.local>
Co-authored-by: Matthew <heimixiaozhuang@zju.edu.cn>

* feat(memos): chunk batch reflection scoring

* Fix #1897: bug(memos-local-plugin): Hermes background model status checks can trigger paid  (#1899)

* Fix #1897: fix(memos-local-plugin): add LLM circuit breaker for terminal provider errors

Issue #1897 reported ~12,900 paid LLM requests in 24 h on Hermes
against a DeepSeek key with insufficient balance. The local
`system_model_status` row count (12,900) closely tracked the
provider-side `request_count` (11,344) for the same billing window.
The naming is misleading: `system_model_status` is not a health probe;
it is the audit row written once per LLM call (ok / fallback / error)
inside `core/llm/client.ts`. With no circuit breaker, every pipeline
subscriber (capture / session-relation / reward / L2 / L3 / skill /
retrieval LLM filter / world-model) kept firing on every turn / closed
episode / induction, generating one paid request each.

Add a per-`LlmClient` circuit breaker:

- Trips on terminal errors: HTTP 401/402/403 or messages containing
  `insufficient balance` / `invalid api key` / `unauthorized` /
  `account suspended` / `billing`.
- Open: short-circuits subsequent calls inside the facade without
  contacting the provider. Throws `MemosError(LLM_UNAVAILABLE)` with
  `details.circuitOpen=true` so existing catch blocks still work.
- Half-open after cool-down (default 5 min, configurable, min 30 s):
  next call probes the provider; success closes the breaker, terminal
  failure re-opens it for another cool-down.
- Host fallback rescues a call without tripping the breaker —
  fallback exists precisely to keep going when the primary is down.
- Coalesces `system_model_status="circuit_open"` audit rows to at
  most one per ~25 s while the breaker stays open, so we don't
  replace paid spam with audit-row spam.
- Exposes `circuitOpen` / `circuitOpenUntil` / `circuitOpenedReason`
  via `LlmClientStats` for the Overview viewer card.
- Enabled by default; legacy behaviour available via
  `circuitBreaker.enabled = false`.

Tests: 9 new vitest cases under `tests/unit/llm/client.test.ts`
covering trip on 402, trip on "insufficient balance" message, no
trip on generic transient, coalescing, half-open close on success,
host-fallback rescues without trip, disabled mode, stats fields,
and re-open on terminal probe failure. All 59 LLM and 28 pipeline
tests pass; `tsc --noEmit` clean.

Out of scope (tracked separately): 429 `Retry-After` handling
(issue #1620), per-tool rate limits, daily budget caps.

* Fix LLM breaker with host fallback

---------

Co-authored-by: autodev-bot <autodev@memtensor.local>
Co-authored-by: Jiang <33757498+hijzy@users.noreply.github.com>
Co-authored-by: GU TIANCHUN <96930846+TianchunGu@users.noreply.github.com>
Co-authored-by: Dubberman <48425266+whipser030@users.noreply.github.com>

---------

Co-authored-by: Memtensor-AI <project@memtensor.cn>
Co-authored-by: MemOS AutoDev <autodev@memtensor.ai>
Co-authored-by: Matthew <heimixiaozhuang@zju.edu.cn>
Co-authored-by: MemOS AutoDev <autodev@memos.ai>
Co-authored-by: MemOS AutoDev Bot <autodev@memtensor.local>
Co-authored-by: Jiang <33757498+hijzy@users.noreply.github.com>
Co-authored-by: GU TIANCHUN <96930846+TianchunGu@users.noreply.github.com>
Co-authored-by: Dubberman <48425266+whipser030@users.noreply.github.com>

* Revert "feat: chunk batch reflection scoring (#1957)" (#2145)

This reverts commit 21a27ed.

Co-authored-by: jiachengzhen <jiacz@memtensor.cn>

* test: stub overview count methods

---------

Co-authored-by: jiachengzhen <jiacz@memtensor.cn>
Co-authored-by: Memtensor-AI <project@memtensor.cn>
Co-authored-by: MemOS AutoDev <autodev@memtensor.ai>
Co-authored-by: Matthew <heimixiaozhuang@zju.edu.cn>
Co-authored-by: MemOS AutoDev <autodev@memos.ai>
Co-authored-by: MemOS AutoDev Bot <autodev@memtensor.local>
Co-authored-by: Jiang <33757498+hijzy@users.noreply.github.com>
Co-authored-by: GU TIANCHUN <96930846+TianchunGu@users.noreply.github.com>
Co-authored-by: Dubberman <48425266+whipser030@users.noreply.github.com>
Co-authored-by: zhaxi <syzsunshine219@gmail.com>

* fix(memos-local-openclaw): declare contracts.tools in plugin manifest (#1757)

OpenClaw runtime requires every plugin to declare contracts.tools
before registering agent tools. Without it, the gateway logs the
following warning on every registration:

    [plugins] plugin must declare contracts.tools before registering
    agent tools (plugin=memos-local-openclaw-plugin, source=index.js)

In a production OpenClaw deployment with active memos plugin, this
warning flooded gateway.err.log at ~1300 lines/day, masking real
errors and bloating logs by ~33MB/day.

This commit declares all 20 tools the plugin actually registers
(grep'd via `api.registerTool(...)` calls in index.ts):

  - memory_*: get, search, share, timeline, unshare, viewer, write_public
  - network_*: memory_detail, skill_pull, team_info
  - skill_*: file_get, files, get, install, publish, search, unpublish
  - task_*: share, summary, unshare

Verified: post-deploy gateway.err.log warning count = 0.

Refs: contracts.tools field per OpenClaw plugin spec (see
openclaw-lark and similar plugins as reference).

Co-authored-by: Richard-JC-Yan <150349207+Richard-JC-Yan@users.noreply.github.com>
Co-authored-by: jiachengzhen <jiacz@memtensor.cn>

* Fix #2148: [Bug] captureRunner uses reflectLlm (skill-evolver) for batch reflection — shoul (#2151)

* fix(plugin): wire capture reflection to main llm (#2148)

Capture batch reflection is a JSON-output task. When operators configure
skillEvolver.enableThinking=true alongside llm.enableThinking=false
(the typical Qwen3 setup), passing `deps.reflectLlm` into the capture
runner made the batch reflection run on the thinking-enabled skill-evolver
model, which emits <think> blocks that break JSON parsing and produce
`malformed JSON` errors during capture summarisation. Route the capture
runner's reflectLlm slot to `deps.llm` so JSON tasks stay on the
non-thinking main model, matching the l3Llm pattern from #1959.

- deps.ts: captureRunner.reflectLlm = deps.llm
- tests: regression guard in tests/unit/pipeline/capture-reflect-llm-wiring.test.ts

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(plugin): clarify reflectLlm exposure comment (#2148 OCR follow-up)

Enumerate the two read-only consumers that still reach `deps.reflectLlm`
(reward-runner evaluator metadata + Overview health card via
`resolveSkillEvolver`) and note that no other subscriber is wired to
it. Prevents future readers from misreading the previous prose as
"skill / capture also use reflectLlm".

---------

Co-authored-by: autodev-bot <autodev@memtensor.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: MemTensor Bot <bot@memtensor.local>
Co-authored-by: jiachengzhen <jiacz@memtensor.cn>

* test(logging): verify standalone bootstrap writes memos.log (#2147) (#2150)

* fix(local-plugin): call initLogger in bootstrapMemoryCoreFull

`bootstrapMemoryCoreFull` in `apps/memos-local-plugin/core/pipeline/memory-core.ts`
never invoked `initLogger(config, home)` after `loadConfig(home)`. The logger
stayed in the `bootstrapConsoleOnly()` fallback (console + memory buffer +
SSE broadcast only), so the `FileRotatingTransport` / `JsonlEventsTransport`
sinks were never wired and `MEMOS_HOME/logs/` (memos.log, error.log,
audit.log, llm.jsonl, perf.jsonl, events.jsonl) was left empty even though
`config.logging.file.enabled` defaults to true.

The fix adds `initLogger` to the logger import and calls it after the config
is resolved and before the first `rootLogger.child(...)` call. `initLogger`
is idempotent and swaps the active root in place, so existing child handles
keep working.

A new regression test `tests/unit/pipeline/bootstrap-init-logger.test.ts`
boots the pipeline against a tmp home, emits a marker line, and asserts
that `memos.log` is created on disk and contains the marker. It fails on
the previous code (ENOENT for memos.log) and passes with this fix.

Fixes #2147

* style(hermes): format daemon manager

---------

Co-authored-by: MemOS AutoDev <autodev@memtensor.local>
Co-authored-by: jiachengzhen <jiacz@memtensor.cn>

* fix: align generated skill guidance (#2100)

* feat(api): align OpenMem v1 SDK with cloud API

* fix: align generated skill guidance

* feat(api): align OpenMem v1 SDK with cloud API

* fix(memos-local): include json hint in user messages (#1756)

Co-authored-by: shinetata <149466187+shinetata@users.noreply.github.com>

---------

Co-authored-by: dingliang <2650876010@qq.com>
Co-authored-by: Qi Weng <bittergreen.wengqi@hotmail.com>
Co-authored-by: de1ty <7804799+de1tydev@users.noreply.github.com>
Co-authored-by: shinetata <149466187+shinetata@users.noreply.github.com>
Co-authored-by: jiachengzhen <jiacz@memtensor.cn>

* fix: Add dedicated document LLM, improve Markdown detection and context-aware structured extraction

---------

Co-authored-by: Memtensor-AI <project@memtensor.cn>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: bittergreen <bittergreen.wengqi@hotmail.com>
Co-authored-by: dingliang <2650876010@qq.com>
Co-authored-by: Erick Wipprecht <53452309+chiefmojo@users.noreply.github.com>
Co-authored-by: jiachengzhen <jiacz@memtensor.cn>
Co-authored-by: shinetata <149466187+shinetata@users.noreply.github.com>
Co-authored-by: Wenqiang Wei <46308778+endxxxx@users.noreply.github.com>
Co-authored-by: MemOS AutoDev <autodev@memtensor.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: memos-autodev[bot] <autodev-bot@users.noreply.github.com>
Co-authored-by: HarveyXiang <harvey_xiang@163.com>
Co-authored-by: autodev-bot <autodev@memtensor.local>
Co-authored-by: Jiang <33757498+hijzy@users.noreply.github.com>
Co-authored-by: GU TIANCHUN <96930846+TianchunGu@users.noreply.github.com>
Co-authored-by: Dubberman <48425266+whipser030@users.noreply.github.com>
Co-authored-by: Fayenix <fayenix@users.noreply.github.com>
Co-authored-by: de1ty <7804799+de1tydev@users.noreply.github.com>
Co-authored-by: MemOS AutoDev <autodev@memtensor.ai>
Co-authored-by: Matthew <heimixiaozhuang@zju.edu.cn>
Co-authored-by: MemOS AutoDev <autodev@memos.ai>
Co-authored-by: Richard-JC-Yan <128337844+Richard-JC-Yan@users.noreply.github.com>
Co-authored-by: Richard-JC-Yan <150349207+Richard-JC-Yan@users.noreply.github.com>
Co-authored-by: MemTensor Bot <bot@memtensor.local>
Co-authored-by: Ziyang Guo <121015044+RerankerGuo@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:plugin OpenClaw & Hermes status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants