From b85149c078ead57c43539ef278586f8ee401a6cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=91=E5=B8=83=E6=9E=97?= <11641432+heiheiyouyou@user.noreply.gitee.com> Date: Wed, 13 May 2026 15:50:24 +0800 Subject: [PATCH 01/38] change name --- apps/memos-local-plugin/ARCHITECTURE.md | 6 +-- apps/memos-local-plugin/adapters/README.md | 2 +- .../hermes/memos_provider/__init__.py | 32 ++++++------- .../adapters/openclaw/README.md | 4 +- .../adapters/openclaw/bridge.ts | 8 ++-- .../adapters/openclaw/index.ts | 32 +++++++++---- .../adapters/openclaw/tools.ts | 48 +++++++++---------- apps/memos-local-plugin/agent-contract/dto.ts | 6 +-- .../agent-contract/memory-core.ts | 2 +- apps/memos-local-plugin/core/config/schema.ts | 4 +- .../core/llm/prompts/retrieval-filter.ts | 4 +- .../core/pipeline/memory-core.ts | 16 +++---- .../core/retrieval/ALGORITHMS.md | 2 +- .../core/retrieval/README.md | 4 +- .../core/retrieval/injector.ts | 24 +++++----- .../core/retrieval/llm-filter.ts | 2 +- .../core/retrieval/retrieve.ts | 2 +- .../core/retrieval/types.ts | 2 +- .../core/storage/repos/api_logs.ts | 2 +- .../core/telemetry/sender.ts | 4 +- apps/memos-local-plugin/core/types.ts | 4 +- .../docs/CONFIG-ADVANCED.md | 4 +- .../docs/DEMO_TaskCLI_OpenClaw_test.md | 26 +++++----- .../docs/E2E_TEST_SCENARIO.md | 10 ++-- .../docs/MANUAL_E2E_TESTING.md | 6 +-- .../PROMPT-INJECTION-AND-RETRIEVAL-FILTER.md | 16 +++---- apps/memos-local-plugin/install.ps1 | 18 ++++++- apps/memos-local-plugin/install.sh | 41 +++++++++------- apps/memos-local-plugin/openclaw.plugin.json | 12 ++--- .../server/routes/api-logs.ts | 6 +-- .../server/routes/import-export.ts | 2 +- .../server/routes/metrics.ts | 6 +-- .../tests/python/test_bridge_client.py | 42 ++++++++-------- .../python/test_hermes_provider_pipeline.py | 2 +- .../unit/adapters/hermes-persistence.test.ts | 6 +-- .../unit/adapters/hermes-protocol.test.ts | 4 +- .../unit/adapters/openclaw-bridge.test.ts | 26 +++++----- .../tests/unit/adapters/openclaw-e2e.test.ts | 8 ++-- .../unit/agent-contract/contract.test.ts | 4 +- .../tests/unit/install/install-sh.test.ts | 8 +++- .../tests/unit/retrieval/injector.test.ts | 27 ++++++----- .../tests/unit/retrieval/integration.test.ts | 2 +- .../unit/retrieval/query-builder.test.ts | 4 +- .../tests/unit/server/http.test.ts | 4 +- .../tests/unit/storage/end-to-end.test.ts | 2 +- .../tests/unit/telemetry/sender.test.ts | 2 +- .../tests/unit/viewer/tasks-chat.test.ts | 4 +- .../viewer/src/stores/i18n.ts | 6 +-- .../viewer/src/styles/components.css | 2 +- .../viewer/src/views/LogsView.tsx | 28 +++++------ .../viewer/src/views/OverviewView.tsx | 2 +- apps/memos-local-plugin/website/index.html | 12 ++--- 52 files changed, 300 insertions(+), 252 deletions(-) diff --git a/apps/memos-local-plugin/ARCHITECTURE.md b/apps/memos-local-plugin/ARCHITECTURE.md index 7814a2d8f..e7713453b 100644 --- a/apps/memos-local-plugin/ARCHITECTURE.md +++ b/apps/memos-local-plugin/ARCHITECTURE.md @@ -167,7 +167,7 @@ heavyweight client today. Standard OpenClaw plugin. Imports `core/` directly. Provides: - `plugin.ts` — `definePluginEntry` wiring; passes config + paths into `createMemoryCore`. -- `tools.ts` — `memory_search`, `memory_get`, `memory_timeline` tool definitions. +- `tools.ts` — `memos_search`, `memos_get`, `memos_timeline` tool definitions. - `hooks.ts` — `onConversationTurn`, `onShutdown`, etc. - `host-llm-bridge.ts` — when `llm.fallback_to_host: true`, route LLM calls through the OpenClaw host's LLM rather than failing. @@ -237,7 +237,7 @@ to this codebase: | Trigger | What runs | Where it lands | |---------------------------------------------------|--------------------------------------------|--------------------------------------------| | New user turn arrives (`onConversationTurn`) | `turnStartRetrieve` — full Tier-1+2+3 | Prepended as `memos_context` to this turn | -| LLM asks for `memory_search` / `memory_timeline` | `toolDrivenRetrieve` — Tier-1+2, no Tier-3 | Returned as the tool's result | +| LLM asks for `memos_search` / `memos_timeline` | `toolDrivenRetrieve` — Tier-1+2, no Tier-3 | Returned as the tool's result | | LLM asks for `skill.` directly | `skillInvokeRetrieve` — the named skill | Returned as the tool's result (cached) | | SubAgent starts (`onSubAgentStart`) | `subAgentRetrieve` — Tier-1+2 scoped to sub-agent role | Prepended to the sub-agent's first turn | | Decision-repair signal fires (see §4.3) | `repairRetrieve` — targeted preference/anti-pattern lookup | Prepended to the **next** LLM step | @@ -275,7 +275,7 @@ agent.turn(input) │ └── tier3 (world-model, top-K=2) └── returns InjectionPacket to adapter ─── agent.execute - ├── (optional) tool call: memory_search + ├── (optional) tool call: memos_search │ └── orchestrator.toolDrivenRetrieve (lightweight; no tier3) ├── (optional) tool call: skill. │ └── orchestrator.skillInvokeRetrieve (single skill, cached) diff --git a/apps/memos-local-plugin/adapters/README.md b/apps/memos-local-plugin/adapters/README.md index d451ec789..a0df0b876 100644 --- a/apps/memos-local-plugin/adapters/README.md +++ b/apps/memos-local-plugin/adapters/README.md @@ -26,7 +26,7 @@ adapters/ │ ├── README.md │ ├── openclaw-api.ts # locally re-declared OpenClaw SDK types │ ├── bridge.ts # OpenClaw events ↔ MemoryCore DTOs -│ ├── tools.ts # memory_search, memory_get, … tool registrations +│ ├── tools.ts # memos_search, memos_get, … tool registrations │ └── index.ts # register(api) — plugin entry point └── hermes/ # hermes-agent plugin (Python, out-of-process) ├── README.md diff --git a/apps/memos-local-plugin/adapters/hermes/memos_provider/__init__.py b/apps/memos-local-plugin/adapters/hermes/memos_provider/__init__.py index f93443c8c..7609c948a 100644 --- a/apps/memos-local-plugin/adapters/hermes/memos_provider/__init__.py +++ b/apps/memos-local-plugin/adapters/hermes/memos_provider/__init__.py @@ -337,14 +337,14 @@ def initialize(self, session_id: str, **kwargs: Any) -> None: # type: ignore[ov def system_prompt_block(self) -> str: # type: ignore[override] return ( "# MemOS Memory\n" - "Persistent long-term memory is active. Call `memory_search`, " - "`memory_get`, `memory_timeline`, `memory_environment`, " - "`skill_list`, or `skill_get` when prior context or learned " + "Persistent long-term memory is active. Call `memos_search`, " + "`memos_get`, `memos_timeline`, `memos_environment`, " + "`memos_skill_list`, or `memos_skill_get` when prior context or learned " "procedures would help. Relevant memories are automatically " "injected at the start of every turn.\n\n" "**Not the same as repo skills:** Hermes' `` / " "`skill_view(name=…)` load **repository SKILL.md** files. " - "`skill_get` / `skill_list` refer to **MemOS-crystallized** " + "`memos_skill_get` / `memos_skill_list` refer to **MemOS-crystallized** " "skills (learned from your runs). If both apply, you may use " "both: repo skills for product conventions, MemOS skills for " "workflows proven on *your* past tasks." @@ -1013,7 +1013,7 @@ def _int_arg(args: dict[str, Any], key: str, default: int, lower: int, upper: in def get_tool_schemas(self) -> list[dict[str, Any]]: # type: ignore[override] return [ { - "name": "memory_search", + "name": "memos_search", "description": ( "Search the local MemOS memory (traces, policies, world models, skills). " "Prefer this before claiming prior context is unavailable." @@ -1041,7 +1041,7 @@ def get_tool_schemas(self) -> list[dict[str, Any]]: # type: ignore[override] }, }, { - "name": "memory_get", + "name": "memos_get", "description": ( "Fetch the full body of a memory item by id. `kind` can be " '"trace" (default), "policy", or "world_model".' @@ -1060,7 +1060,7 @@ def get_tool_schemas(self) -> list[dict[str, Any]]: # type: ignore[override] }, }, { - "name": "memory_timeline", + "name": "memos_timeline", "description": "Return the ordered traces for an episode id.", "parameters": { "type": "object", @@ -1072,7 +1072,7 @@ def get_tool_schemas(self) -> list[dict[str, Any]]: # type: ignore[override] }, }, { - "name": "skill_list", + "name": "memos_skill_list", "description": ( "List callable skills the agent can invoke. Filter by status " "(candidate | active | archived)." @@ -1094,7 +1094,7 @@ def get_tool_schemas(self) -> list[dict[str, Any]]: # type: ignore[override] }, }, { - "name": "memory_environment", + "name": "memos_environment", "description": ( "Return accumulated environment knowledge (L3 world models): " "structural facts, behavioral rules, and project constraints." @@ -1116,7 +1116,7 @@ def get_tool_schemas(self) -> list[dict[str, Any]]: # type: ignore[override] }, }, { - "name": "skill_get", + "name": "memos_skill_get", "description": "Return the full invocation guide for a crystallized skill.", "parameters": { "type": "object", @@ -1130,7 +1130,7 @@ def handle_tool_call(self, tool_name: str, args: dict[str, Any], **_kwargs: Any) if not self._bridge: return json.dumps({"error": "bridge not connected"}) try: - if tool_name == "memory_search": + if tool_name == "memos_search": query = (args.get("query") or "").strip() if not query: return json.dumps({"error": "missing query"}) @@ -1152,7 +1152,7 @@ def handle_tool_call(self, tool_name: str, args: dict[str, Any], **_kwargs: Any) params, ) return json.dumps({"hits": resp.get("hits", [])}) - if tool_name == "memory_get": + if tool_name == "memos_get": item_id = (args.get("id") or "").strip() if not item_id: return json.dumps({"error": "missing id"}) @@ -1209,7 +1209,7 @@ def handle_tool_call(self, tool_name: str, args: dict[str, Any], **_kwargs: Any) "meta": meta, } ) - if tool_name == "memory_timeline": + if tool_name == "memos_timeline": resp = self._bridge.request( "memory.timeline", { @@ -1220,13 +1220,13 @@ def handle_tool_call(self, tool_name: str, args: dict[str, Any], **_kwargs: Any) limit = self._int_arg(args, "limit", 20, 1, 100) traces = resp.get("traces", [])[:limit] return json.dumps({"traces": traces}) - if tool_name == "skill_list": + if tool_name == "memos_skill_list": limit = self._int_arg(args, "limit", 10, 1, 50) params = {"limit": limit, "namespace": self._runtime_namespace()} if args.get("status"): params["status"] = args["status"] return json.dumps(self._bridge.request("skill.list", params)) - if tool_name == "memory_environment": + if tool_name == "memos_environment": query = (args.get("query") or "").strip() limit = self._int_arg(args, "limit", 5, 1, 30) if not query: @@ -1275,7 +1275,7 @@ def handle_tool_call(self, tool_name: str, args: dict[str, Any], **_kwargs: Any) "queried": True, } ) - if tool_name == "skill_get": + if tool_name == "memos_skill_get": skill_id = (args.get("id") or "").strip() if not skill_id: return json.dumps({"error": "missing id"}) diff --git a/apps/memos-local-plugin/adapters/openclaw/README.md b/apps/memos-local-plugin/adapters/openclaw/README.md index 0a5efb5d8..61734f775 100644 --- a/apps/memos-local-plugin/adapters/openclaw/README.md +++ b/apps/memos-local-plugin/adapters/openclaw/README.md @@ -13,8 +13,8 @@ adapter exposes a `register(api)` function that wires our - **Turn hooks**: `before_prompt_build` → `onTurnStart`, `agent_end` → `onTurnEnd`. -- **Memory tools**: `memory_search`, `memory_get`, `memory_timeline`, - `skill_list`, `skill_get` — all thin wrappers around `MemoryCore`. +- **Memory tools**: `memos_search`, `memos_get`, `memos_timeline`, + `memos_skill_list`, `memos_skill_get` — all thin wrappers around `MemoryCore`. - **Tool-outcome observation**: every tool call's success/failure is forwarded to `recordToolOutcome` so decision-repair can react on the next turn. diff --git a/apps/memos-local-plugin/adapters/openclaw/bridge.ts b/apps/memos-local-plugin/adapters/openclaw/bridge.ts index aa81f92e8..fcb11fd6b 100644 --- a/apps/memos-local-plugin/adapters/openclaw/bridge.ts +++ b/apps/memos-local-plugin/adapters/openclaw/bridge.ts @@ -483,7 +483,7 @@ function stripOpenClawUserEnvelope(raw: string): string { "", ); text = text.replace( - /## Memory system\n+No memories were automatically recalled[^\n]*(?:\n[^\n]*memory_search[^\n]*)*/gi, + /## Memory system\n+No memories were automatically recalled[^\n]*(?:\n[^\n]*memos_search[^\n]*)*/gi, "", ); @@ -802,7 +802,7 @@ const CONTEXT_CLOSE = ""; * When the store is cold (no hits), we still emit a short "memory * tools are available" hint — the legacy `memos-local-openclaw` * adapter does the same via `noRecallHint`, and without it the LLM - * has no reason to call `memory_search` at the start of a + * has no reason to call `memos_search` at the start of a * conversation. The hint is kept *small* so repeated turns don't * bloat the system prompt. */ @@ -817,11 +817,11 @@ export function renderContextBlock( } if (opts.hintWhenEmpty === false) return ""; // Cold-start hint — mirrors the legacy adapter's behaviour so the - // model is nudged to reach for `memory_search` even on the first + // model is nudged to reach for `memos_search` even on the first // turn of a fresh session. const hint = [ "No prior memories matched this query — the store may simply be cold.", - "You can still call `memory_search` with a shorter or rephrased query", + "You can still call `memos_search` with a shorter or rephrased query", "if you expect there to be relevant past context.", ].join(" "); return `${CONTEXT_OPEN}\n${hint}\n${CONTEXT_CLOSE}`; diff --git a/apps/memos-local-plugin/adapters/openclaw/index.ts b/apps/memos-local-plugin/adapters/openclaw/index.ts index acb0bf36a..55c0a442d 100644 --- a/apps/memos-local-plugin/adapters/openclaw/index.ts +++ b/apps/memos-local-plugin/adapters/openclaw/index.ts @@ -138,7 +138,7 @@ async function createRuntime(api: OpenClawPluginApi): Promise { await core.init(); // Anonymous ARMS telemetry. Mirrors `bridge.cts`'s setup so OpenClaw - // emits the same `plugin_started` / `daily_active` / `memory_search` + // emits the same `plugin_started` / `daily_active` / `memos_search` // / `memory_ingested` / `feedback_submitted` / `viewer_opened` // events under the same `memos_local_hermes_v2` group as Hermes. // Without this every OpenClaw user was invisible in ARMS — only the @@ -242,29 +242,43 @@ function register(api: OpenClawPluginApi): void { // fails later. api.registerMemoryCapability?.({ promptBuilder: ({ availableTools }) => { - const hasSearch = availableTools.has("memory_search"); - const hasGet = availableTools.has("memory_get"); - const hasTimeline = availableTools.has("memory_timeline"); - const hasEnv = availableTools.has("memory_environment"); - if (!hasSearch && !hasGet && !hasTimeline && !hasEnv) return []; + const hasSearch = availableTools.has("memos_search"); + const hasGet = availableTools.has("memos_get"); + const hasTimeline = availableTools.has("memos_timeline"); + const hasEnv = availableTools.has("memos_environment"); + const hasSkillList = availableTools.has("memos_skill_list"); + const hasSkillGet = availableTools.has("memos_skill_get"); + if (!hasSearch && !hasGet && !hasTimeline && !hasEnv && !hasSkillList && !hasSkillGet) { + return []; + } const lines: string[] = [ "## Memory (MemOS Local)", "This workspace uses MemOS Local — a self-evolving layered memory (L1/L2/L3 + Skills).", ]; if (hasSearch) { lines.push( - "- `memory_search` — search prior traces, policies, world models, and skills.", + "- `memos_search` — search prior traces, policies, world models, and skills.", ); } if (hasEnv) { lines.push( - "- `memory_environment` — list / query accumulated environment knowledge " + + "- `memos_environment` — list / query accumulated environment knowledge " + "(project layout, behavioural rules, constraints). Use before exploring an unfamiliar area.", ); } if (hasGet || hasTimeline) { lines.push( - "- `memory_get` / `memory_timeline` — fetch full bodies + episode timelines.", + "- `memos_get` / `memos_timeline` — fetch full bodies + episode timelines.", + ); + } + if (hasSkillList) { + lines.push( + "- `memos_skill_list` — list MemOS-crystallized skills learned from prior runs.", + ); + } + if (hasSkillGet) { + lines.push( + "- `memos_skill_get` — load the full invocation guide for a MemOS skill.", ); } lines.push( diff --git a/apps/memos-local-plugin/adapters/openclaw/tools.ts b/apps/memos-local-plugin/adapters/openclaw/tools.ts index 673d37dc8..36639eafb 100644 --- a/apps/memos-local-plugin/adapters/openclaw/tools.ts +++ b/apps/memos-local-plugin/adapters/openclaw/tools.ts @@ -176,10 +176,10 @@ async function resolveCore(opts: ToolsOptions): Promise { export function registerOpenClawTools(api: OpenClawPluginApi, opts: ToolsOptions): void { const bodyCap = opts.maxBodyChars ?? DEFAULT_BODY_CAP; - // ── memory_search ── + // ── memos_search ── api.registerTool( (ctx: OpenClawPluginToolContext): AgentToolDescriptor => ({ - name: "memory_search", + name: "memos_search", label: "Memory Search", description: "Search the local MemOS memory (traces + policies + world models + skills). " + @@ -213,13 +213,13 @@ export function registerOpenClawTools(api: OpenClawPluginApi, opts: ToolsOptions return textToolResult(details, formatHitList(details.hits)); }, }), - { name: "memory_search" }, + { name: "memos_search" }, ); - // ── memory_get ── + // ── memos_get ── api.registerTool( (ctx: OpenClawPluginToolContext): AgentToolDescriptor => ({ - name: "memory_get", + name: "memos_get", label: "Memory Get", description: 'Fetch the full body of a memory item by id. `kind` can be "trace" (default), ' + @@ -291,13 +291,13 @@ export function registerOpenClawTools(api: OpenClawPluginApi, opts: ToolsOptions return textToolResult(details, `${wm.title}\n\n${details.body}`.trim()); }, }), - { name: "memory_get" }, + { name: "memos_get" }, ); - // ── memory_timeline ── + // ── memos_timeline ── api.registerTool( (ctx: OpenClawPluginToolContext): AgentToolDescriptor => ({ - name: "memory_timeline", + name: "memos_timeline", label: "Memory Timeline", description: "Return the ordered traces inside a single episode. Useful for reconstructing " + @@ -327,13 +327,13 @@ export function registerOpenClawTools(api: OpenClawPluginApi, opts: ToolsOptions return textToolResult(details, text); }, }), - { name: "memory_timeline" }, + { name: "memos_timeline" }, ); - // ── skill_list ── + // ── memos_skill_list ── api.registerTool( (ctx: OpenClawPluginToolContext): AgentToolDescriptor => ({ - name: "skill_list", + name: "memos_skill_list", label: "Skill List", description: "List callable skills the agent can invoke. Filter by status (candidate | active | archived).", @@ -363,10 +363,10 @@ export function registerOpenClawTools(api: OpenClawPluginApi, opts: ToolsOptions return textToolResult(details, text); }, }), - { name: "skill_list" }, + { name: "memos_skill_list" }, ); - // ── memory_environment ── + // ── memos_environment ── // // Dedicated Tier-3 lookup. The turn-start injector already folds // environment knowledge into `prependContext`, but during a long @@ -377,7 +377,7 @@ export function registerOpenClawTools(api: OpenClawPluginApi, opts: ToolsOptions // knowledge on demand. api.registerTool( (ctx: OpenClawPluginToolContext): AgentToolDescriptor => ({ - name: "memory_environment", + name: "memos_environment", label: "Environment Knowledge", description: "Return the agent's accumulated environment knowledge (L3 world models) — " + @@ -437,13 +437,13 @@ export function registerOpenClawTools(api: OpenClawPluginApi, opts: ToolsOptions return textToolResult(details, text); }, }), - { name: "memory_environment" }, + { name: "memos_environment" }, ); - // ── skill_get ── + // ── memos_skill_get ── api.registerTool( (ctx: OpenClawPluginToolContext): AgentToolDescriptor => ({ - name: "skill_get", + name: "memos_skill_get", label: "Skill Get", description: "Return the full invocation guide for a crystallized skill.", parameters: SkillGetParams, @@ -481,7 +481,7 @@ export function registerOpenClawTools(api: OpenClawPluginApi, opts: ToolsOptions return textToolResult(details, `${skill.name}\n\n${skill.invocationGuide}`.trim()); }, }), - { name: "skill_get" }, + { name: "memos_skill_get" }, ); } @@ -506,10 +506,10 @@ function topKParams( /** Exposed for tests + documentation. */ export const TOOL_SCHEMAS = { - memory_search: MemorySearchParams, - memory_get: MemoryGetParams, - memory_timeline: MemoryTimelineParams, - memory_environment: EnvironmentQueryParams, - skill_list: SkillListParams, - skill_get: SkillGetParams, + memos_search: MemorySearchParams, + memos_get: MemoryGetParams, + memos_timeline: MemoryTimelineParams, + memos_environment: EnvironmentQueryParams, + memos_skill_list: SkillListParams, + memos_skill_get: SkillGetParams, } as const; diff --git a/apps/memos-local-plugin/agent-contract/dto.ts b/apps/memos-local-plugin/agent-contract/dto.ts index 7428b8d7a..657b1b33f 100644 --- a/apps/memos-local-plugin/agent-contract/dto.ts +++ b/apps/memos-local-plugin/agent-contract/dto.ts @@ -430,9 +430,9 @@ export interface SkillDTO extends OwnershipDTO { } | null; /** Last user edit through the viewer's edit modal. */ editedAt?: EpochMs; - /** Number of successful `skill_get` calls that loaded this skill. */ + /** Number of successful `memos_skill_get` calls that loaded this skill. */ usageCount?: number; - /** Last successful `skill_get` time. */ + /** Last successful `memos_skill_get` time. */ lastUsedAt?: EpochMs | null; } @@ -602,7 +602,7 @@ export interface ToolDrivenCtx { namespace?: RuntimeNamespace; sessionId: SessionId; episodeId?: EpisodeId; - /** Which memory tool was called (memory_search / memory_timeline / …). */ + /** Which memory tool was called (memos_search / memos_timeline / …). */ tool: string; /** The tool's input arguments verbatim. */ args: Record; diff --git a/apps/memos-local-plugin/agent-contract/memory-core.ts b/apps/memos-local-plugin/agent-contract/memory-core.ts index aa19dd692..87492fbc4 100644 --- a/apps/memos-local-plugin/agent-contract/memory-core.ts +++ b/apps/memos-local-plugin/agent-contract/memory-core.ts @@ -389,7 +389,7 @@ export interface MemoryCore { /** * Paged listing of the rich api_logs table ({@link ApiLogDTO}). - * Fuels the viewer's Logs page — shows every memory_search and + * Fuels the viewer's Logs page — shows every memos_search and * memory_add call with the full input/output JSON. */ listApiLogs(input?: { diff --git a/apps/memos-local-plugin/core/config/schema.ts b/apps/memos-local-plugin/core/config/schema.ts index 675cade70..84ca6f6b4 100644 --- a/apps/memos-local-plugin/core/config/schema.ts +++ b/apps/memos-local-plugin/core/config/schema.ts @@ -384,8 +384,8 @@ const AlgorithmSchema = Type.Object({ /** * How Tier-1 skills are surfaced in the injected prompt: * - "summary" (default): inject only `name + η + 1-line summary + - * a `skill_get(id="…")` hint`. The agent decides whether to - * fetch the full procedure via the `skill_get` tool. Keeps the + * a `memos_skill_get(id="…")` hint`. The agent decides whether to + * fetch the full procedure via the `memos_skill_get` tool. Keeps the * prompt small and avoids paying for skills the agent never * uses. * - "full": inline the entire `invocationGuide` body (legacy diff --git a/apps/memos-local-plugin/core/llm/prompts/retrieval-filter.ts b/apps/memos-local-plugin/core/llm/prompts/retrieval-filter.ts index c39da29be..f91a35159 100644 --- a/apps/memos-local-plugin/core/llm/prompts/retrieval-filter.ts +++ b/apps/memos-local-plugin/core/llm/prompts/retrieval-filter.ts @@ -50,7 +50,7 @@ Decision guidance: without such facts should be dropped. - RANK a SKILL when its name / description plausibly addresses the user's sub-problem. The agent decides later whether to call - \`skill_get\` for the full procedure — err on the side of ranking + \`memos_skill_get\` for the full procedure — err on the side of ranking every candidate skill that could plausibly help. - RANK a WORLD-MODEL when its topic matches the domain of the query and the body contains structural information the agent would @@ -75,7 +75,7 @@ After ranking useful candidates, self-report whether that useful set is enough: - \`sufficient: true\` when the useful items plausibly answer the QUERY as-is. - \`sufficient: false\` when the useful items are only a starting point - and the agent should broaden recall (e.g. run \`memory_search\` with + and the agent should broaden recall (e.g. run \`memos_search\` with a different query). ──── Example 1 (React dark mode, RANK 2 useful candidates) ──── diff --git a/apps/memos-local-plugin/core/pipeline/memory-core.ts b/apps/memos-local-plugin/core/pipeline/memory-core.ts index bb222de65..7cc4db942 100644 --- a/apps/memos-local-plugin/core/pipeline/memory-core.ts +++ b/apps/memos-local-plugin/core/pipeline/memory-core.ts @@ -688,7 +688,7 @@ export function createMemoryCore( // ─── Skill lifecycle → api_logs(skill_*) ────────────────────────── // Emit structured rows for the Logs page so users can watch skill // generation / verification / retirement events with the same JSON - // detail the memory_search / memory_add cards show. Event shapes + // detail the memos_search / memory_add cards show. Event shapes // vary per kind — we spread the raw event into `output` (with any // sensitive fields already redacted upstream) rather than hand- // rolling per-kind schemas. @@ -1360,7 +1360,7 @@ export function createMemoryCore( } finally { // Log every retrieval — not just adhoc `searchMemory` calls — // so the viewer's Logs page can show what was recalled for - // each real agent turn. Without this, `memory_search` rows + // each real agent turn. Without this, `memos_search` rows // only showed up when the viewer's search box was used. try { const snippets = packet?.snippets ?? []; @@ -1378,7 +1378,7 @@ export function createMemoryCore( const dropped = candidates.filter((c) => droppedIds.has(c.refId)); const stats = packet ? handle.consumeRetrievalStats(packet.packetId) : null; handle.repos.apiLogs.insert({ - toolName: "memory_search", + toolName: "memos_search", input: { type: "turn_start", agent: turn.agent, @@ -1400,7 +1400,7 @@ export function createMemoryCore( calledAt: startedAt, }); } catch (logErr) { - log.debug("apiLogs.memory_search.turn_start.skipped", { + log.debug("apiLogs.memos_search.turn_start.skipped", { err: logErr instanceof Error ? logErr.message : String(logErr), }); } @@ -2058,7 +2058,7 @@ export function createMemoryCore( ok = false; if (telemetry) { telemetry.trackError( - "memory_search", + "memos_search", err instanceof MemosError ? err.code : "unknown", ); } @@ -2066,7 +2066,7 @@ export function createMemoryCore( } finally { try { handle.repos.apiLogs.insert({ - toolName: "memory_search", + toolName: "memos_search", input: { type: "tool_call", agent: query.agent, @@ -2088,7 +2088,7 @@ export function createMemoryCore( calledAt: startedAt, }); } catch (logErr) { - log.debug("apiLogs.memory_search.skipped", { + log.debug("apiLogs.memos_search.skipped", { err: logErr instanceof Error ? logErr.message : String(logErr), }); } @@ -2904,7 +2904,7 @@ export function createMemoryCore( createdAt: Date.now(), resolvedAt: null, evidence: { - source: "skill_get", + source: "memos_skill_get", }, }); } diff --git a/apps/memos-local-plugin/core/retrieval/ALGORITHMS.md b/apps/memos-local-plugin/core/retrieval/ALGORITHMS.md index 0b2cea6e6..63435a000 100644 --- a/apps/memos-local-plugin/core/retrieval/ALGORITHMS.md +++ b/apps/memos-local-plugin/core/retrieval/ALGORITHMS.md @@ -146,7 +146,7 @@ Callers treat `null` as "don't inject anything". degenerate packets. Values are user-tunable via `algorithm.retrieval.*`. 5. **Tier-1 summary mode** — V7 §2.6 implies the full Skill body is injected at turn start. We default to a *summary* representation - (`name + η + 1-line description + a `skill_get(id="…")` invocation + (`name + η + 1-line description + a `memos_skill_get(id="…")` invocation hint`) so the host model can pull the full procedure on demand instead of bloating every prompt with skills it may never use. Hosts without function calling can opt back into full-body inlining by diff --git a/apps/memos-local-plugin/core/retrieval/README.md b/apps/memos-local-plugin/core/retrieval/README.md index c722f1c6f..fc57414b4 100644 --- a/apps/memos-local-plugin/core/retrieval/README.md +++ b/apps/memos-local-plugin/core/retrieval/README.md @@ -33,7 +33,7 @@ All five return a `RetrievalResult = { packet, stats }`: ```ts import { turnStartRetrieve, // onConversationTurn — full Tier 1+2+3 - toolDrivenRetrieve, // memory_search / memory_timeline / … + toolDrivenRetrieve, // memos_search / memos_timeline / … skillInvokeRetrieve, // agent is about to call skill. subAgentRetrieve, // sub-agent spawned with mission prompt repairRetrieve, // N tool failures → anti-pattern recall @@ -75,7 +75,7 @@ prompt with content the agent may never use. We support two modes via | Mode | What lands in the prompt | When to use | |-----------|----------------------------------------------------------------------------|-------------------------------------------------| -| `summary` (default) | `name`, `η`, `status`, a 1-line summary, plus a `skill_get(id="…")` invocation hint. The footer also lists `skill_get` / `skill_list`. | Tool-calling hosts (OpenClaw, Hermes). Keeps prompts small; the agent calls `skill_get` only for skills it actually wants. | +| `summary` (default) | `name`, `η`, `status`, a 1-line summary, plus a `memos_skill_get(id="…")` invocation hint. The footer also lists `memos_skill_get` / `memos_skill_list`. | Tool-calling hosts (OpenClaw, Hermes). Keeps prompts small; the agent calls `memos_skill_get` only for skills it actually wants. | | `full` | Legacy: full `invocationGuide` body per skill (truncated to 640 chars). | Hosts without function-calling support. | The summary text is the first paragraph of `invocationGuide` (clamped to diff --git a/apps/memos-local-plugin/core/retrieval/injector.ts b/apps/memos-local-plugin/core/retrieval/injector.ts index 69fa6066b..6e26db9c8 100644 --- a/apps/memos-local-plugin/core/retrieval/injector.ts +++ b/apps/memos-local-plugin/core/retrieval/injector.ts @@ -52,7 +52,7 @@ export interface InjectorInput { episodeId: EpisodeId; /** * How Tier-1 skill candidates should be rendered. Defaults to - * `"summary"` — a short descriptor + `skill_get(id="…")` invocation + * `"summary"` — a short descriptor + `memos_skill_get(id="…")` invocation * hint, so the host model decides whether to pull the full guide. */ skillInjectionMode?: SkillInjectionMode; @@ -188,7 +188,7 @@ function renderSnippet(c: TierCandidate, opts: RenderOpts): InjectionSnippet | n * Render a Tier-1 Skill candidate. * * **Summary mode** (default): the prompt only carries a 1-line teaser - * and a `skill_get(id="…")` hint. The host model can call that tool on + * and a `memos_skill_get(id="…")` hint. The host model can call that tool on * demand to fetch the full procedure — keeps prompts small and avoids * paying for skills the agent never needs. * @@ -208,11 +208,13 @@ function renderSkill(c: SkillCandidate, opts: RenderOpts): InjectionSnippet { }; } - const summary = firstLineSummary(c.invocationGuide, opts.skillSummaryChars); - const lines: string[] = []; - if (summary) lines.push(summary); + const description = firstLineSummary(c.invocationGuide, opts.skillSummaryChars); + const lines: string[] = [ + `Name: ${c.skillName}`, + `Description: ${description || "(not provided)"}`, + ]; lines.push( - `→ call \`skill_get(id="${c.refId}")\` to load the full procedure if you decide to use it`, + `→ call \`memos_skill_get(id="${c.refId}")\` to load the full procedure if you decide to use it`, ); return { refKind: "skill", @@ -344,7 +346,7 @@ function renderWorldModel(c: WorldModelCandidate): InjectionSnippet { * When container pip fails, install -dev OS lib first … * * Available follow-up tools: - * - call `memory_search(query=...)` for a shorter, more targeted query + * - call `memos_search(query=...)` for a shorter, more targeted query * ``` * * We deliberately keep the "IMPORTANT" instructions — without them the @@ -373,10 +375,10 @@ function renderWholePacket( if (skills.length > 0) { if (opts.skillMode === "summary") { // In summary mode, frame the section as "candidate skills you can - // call". The bodies already carry the per-skill `skill_get(...)` + // call". The bodies already carry the per-skill `memos_skill_get(...)` // hint, so the agent knows how to expand them on demand. parts.push( - "## Candidate skills (call `skill_get` to load any you decide to use)\n", + "## Candidate skills (call `memos_skill_get` to load any you decide to use)\n", ); } else { parts.push("## Skills\n"); @@ -482,11 +484,11 @@ const HEADER_BY_REASON: Record = { }; const FOOTER_LINES_COMMON: readonly string[] = [ - "- `memory_search(query, maxResults?)` — re-query with a shorter / rephrased string", + "- `memos_search(query, maxResults?)` — re-query with a shorter / rephrased string", ]; const FOOTER_LINES_SKILL_SUMMARY: readonly string[] = [ - "- `skill_get(id)` — load the full procedure/verification of a candidate skill listed above", + "- `memos_skill_get(id)` — load the full procedure/verification of a candidate skill listed above", ]; function footerFor( diff --git a/apps/memos-local-plugin/core/retrieval/llm-filter.ts b/apps/memos-local-plugin/core/retrieval/llm-filter.ts index af54804f5..054d3b270 100644 --- a/apps/memos-local-plugin/core/retrieval/llm-filter.ts +++ b/apps/memos-local-plugin/core/retrieval/llm-filter.ts @@ -76,7 +76,7 @@ export interface FilterResult { /** * The LLM's self-report on whether the *kept* candidates are enough * to answer `query`, or whether the caller should widen recall / - * run a follow-up `memory_search`. `null` when the filter didn't + * run a follow-up `memos_search`. `null` when the filter didn't * run (disabled / passthrough / failure paths). */ sufficient: boolean | null; diff --git a/apps/memos-local-plugin/core/retrieval/retrieve.ts b/apps/memos-local-plugin/core/retrieval/retrieve.ts index 93b62a994..bc5c4b994 100644 --- a/apps/memos-local-plugin/core/retrieval/retrieve.ts +++ b/apps/memos-local-plugin/core/retrieval/retrieve.ts @@ -386,7 +386,7 @@ async function runAll( sessionId: sessionId ?? (`adhoc-session-${ids.span()}` as SessionId), episodeId: episodeId ?? (`adhoc-episode-${ids.span()}` as EpisodeId), // V7 §2.6 — Tier-1 default = "summary" so we surface skill - // descriptors + a `skill_get(...)` invocation hint instead of + // descriptors + a `memos_skill_get(...)` invocation hint instead of // inlining every full guide. Hosts without tool support can flip // this to "full" via `algorithm.retrieval.skillInjectionMode`. skillInjectionMode: deps.config.skillInjectionMode, diff --git a/apps/memos-local-plugin/core/retrieval/types.ts b/apps/memos-local-plugin/core/retrieval/types.ts index e1a486e76..eba793a84 100644 --- a/apps/memos-local-plugin/core/retrieval/types.ts +++ b/apps/memos-local-plugin/core/retrieval/types.ts @@ -269,7 +269,7 @@ export interface RetrievalConfig { /** * V7 §2.6 Tier-1 rendering mode. * - "summary" (default): inject `name + η + first-line summary + - * a `skill_get(id="…")` invocation hint`. Lets the host model + * a `memos_skill_get(id="…")` invocation hint`. Lets the host model * pull the full procedure on demand instead of bloating every * prompt with skills it may never use. * - "full": inline the full `invocationGuide` body (legacy). diff --git a/apps/memos-local-plugin/core/storage/repos/api_logs.ts b/apps/memos-local-plugin/core/storage/repos/api_logs.ts index f0cbb7f13..4d56a4842 100644 --- a/apps/memos-local-plugin/core/storage/repos/api_logs.ts +++ b/apps/memos-local-plugin/core/storage/repos/api_logs.ts @@ -1,6 +1,6 @@ /** * `api_logs` repository — structured log of the user-facing memory - * operations (`memory_search`, `memory_add`). Mirrors the legacy + * operations (`memos_search`, `memory_add`). Mirrors the legacy * `memos-local-openclaw` plugin's table so the new viewer can render * the same rich JSON payloads (candidates, filtered, hub results, * ingestion stats, …). diff --git a/apps/memos-local-plugin/core/telemetry/sender.ts b/apps/memos-local-plugin/core/telemetry/sender.ts index 4a72c7bdf..dced19e7b 100644 --- a/apps/memos-local-plugin/core/telemetry/sender.ts +++ b/apps/memos-local-plugin/core/telemetry/sender.ts @@ -272,7 +272,7 @@ export class Telemetry { } trackTurnStart(agentName: string, latencyMs: number, hitCount: number): void { - this.capture("memory_search", { + this.capture("memos_search", { agent_name: agentName, type: "turn_start", latency_ms: Math.round(latencyMs), @@ -288,7 +288,7 @@ export class Telemetry { } trackMemorySearch(agentName: string, latencyMs: number, hitCount: number): void { - this.capture("memory_search", { + this.capture("memos_search", { agent_name: agentName, type: "adhoc", latency_ms: Math.round(latencyMs), diff --git a/apps/memos-local-plugin/core/types.ts b/apps/memos-local-plugin/core/types.ts index efe49b963..3ecd76847 100644 --- a/apps/memos-local-plugin/core/types.ts +++ b/apps/memos-local-plugin/core/types.ts @@ -305,9 +305,9 @@ export interface SkillRow extends OwnedRow { } | null; /** Last user edit through the viewer's edit modal (migration 009). */ editedAt?: EpochMs | null; - /** Number of successful `skill_get` calls that loaded this skill. */ + /** Number of successful `memos_skill_get` calls that loaded this skill. */ usageCount?: number; - /** Last successful `skill_get` time, or null when never loaded. */ + /** Last successful `memos_skill_get` time, or null when never loaded. */ lastUsedAt?: EpochMs | null; } diff --git a/apps/memos-local-plugin/docs/CONFIG-ADVANCED.md b/apps/memos-local-plugin/docs/CONFIG-ADVANCED.md index eed568197..014e3deea 100644 --- a/apps/memos-local-plugin/docs/CONFIG-ADVANCED.md +++ b/apps/memos-local-plugin/docs/CONFIG-ADVANCED.md @@ -154,9 +154,9 @@ algorithm: smartSeed: true # MMR seed-by-tier only when tier's best clears the relative floor skillInjectionMode: summary # summary (default) | full # summary → Tier-1 skills land in the prompt as `name + η + 1-line - # description + a `skill_get(id="…")` invocation hint`. The + # description + a `memos_skill_get(id="…")` invocation hint`. The # host model loads the full procedure on demand by calling - # the `skill_get` tool. Keeps prompts small. + # the `memos_skill_get` tool. Keeps prompts small. # full → Inline the entire `invocationGuide` body (legacy). Use # this if your host doesn't support tool / function calls. skillSummaryChars: 200 # char cap for the per-skill summary line diff --git a/apps/memos-local-plugin/docs/DEMO_TaskCLI_OpenClaw_test.md b/apps/memos-local-plugin/docs/DEMO_TaskCLI_OpenClaw_test.md index c14c3cc11..ed4cd8efb 100644 --- a/apps/memos-local-plugin/docs/DEMO_TaskCLI_OpenClaw_test.md +++ b/apps/memos-local-plugin/docs/DEMO_TaskCLI_OpenClaw_test.md @@ -185,7 +185,7 @@ sqlite3 ~/.openclaw/memos-plugin/data/memos.db \ - 命令返回 `"stopReason": "stop"` 后**手动 Ctrl+C**(agent 会进入 hub retry 循环,不会自动退出) - 每轮 Ctrl+C 后**等 40-60 秒**让 capture / reward / L2 / L3 / skill 订阅者收尾,再去面板验证 -- **召回看日志页**:每轮 `memory_search` 卡片展开后有「初步召回 / Hub 远端 / LLM 筛选后」三段,被注入 assistant 的就是「LLM 筛选后」 +- **召回看日志页**:每轮 `memos_search` 卡片展开后有「初步召回 / Hub 远端 / LLM 筛选后」三段,被注入 assistant 的就是「LLM 筛选后」 --- @@ -212,8 +212,8 @@ openclaw agent --session-id "$SESSION" --timeout 120 --json --message \ | 记忆 | 多条 step trace(一次 tool call 一条 trace + 一条总结 trace) | | 任务 | 1 个**进行中**的 episode,r_task 暂时 null | | 经验 / 环境认知 / 技能 | 0 | -| 日志 → `memory_search` | **「初步召回」为空** `candidates: []` —— 完美的"冷启动空召回" | -| 日志 | `memory_add` + `memory_search` 各 1 条 | +| 日志 → `memos_search` | **「初步召回」为空** `candidates: []` —— 完美的"冷启动空召回" | +| 日志 | `memory_add` + `memos_search` 各 1 条 | > ✅ 这一步演示了 **L1 trace 写入 + 反思加权 V 框架已就位**。注意 V/α 此刻仍然是 0,要等 Round 2 的 `new_task` 信号触发 R1 的 reward 评分后才回填。 @@ -259,7 +259,7 @@ openclaw agent --session-id "$SESSION" --timeout 120 --json --message \ |---|---| | 任务 → R2 episode | status=closed、r_task≈0.75 | | 经验 | 第 1 条 L2 policy 应已出现(可能 `candidate`,可能升 `active`,看阈值) | -| **日志 → `memory_search`** | **第一次出现 Tier 2 trace 召回!**「初步召回」里有 R1/R2 的 storage 实现 trace,score 0.7+,「LLM 筛选后」保留并**注入 prompt** | +| **日志 → `memos_search`** | **第一次出现 Tier 2 trace 召回!**「初步召回」里有 R1/R2 的 storage 实现 trace,score 0.7+,「LLM 筛选后」保留并**注入 prompt** | > ✅ **生成→召回闭环的第一次显形**:R1/R2 写盘的代码被 Tier 2 召回,进入 R3 的 prompt——assistant 写 SQLite 时能直接参考之前的 storage 风格。 @@ -326,7 +326,7 @@ openclaw agent --session-id "$SESSION" --timeout 120 --json --message \ |---|---| | 经验 | pytest policy `support` 升到 3 | | 技能 | 可能 version 升到 2(rebuild),eta 上调 | -| **日志 → `memory_search`** | **Tier 1 技能召回首次出现** —— 「初步召回」第一条是 Skill:`validate_python_syntax_compile (η=0.5)`,技能 invocation guide 注入 prompt | +| **日志 → `memos_search`** | **Tier 1 技能召回首次出现** —— 「初步召回」第一条是 Skill:`validate_python_syntax_compile (η=0.5)`,技能 invocation guide 注入 prompt | > ✅ **演示第二个高潮**:技能召回首次接管任务,OpenClaw 直接照已结晶的 pytest 模板写。 @@ -401,11 +401,11 @@ openclaw agent --session-id task-cli-demo-recall-show --timeout 150 --json --mes ### 5.3 召回查证 — 看注入 prompt 的实际内容 -#### 5.3.1 数据库查 `memory_search.candidates`(最直接) +#### 5.3.1 数据库查 `memos_search.candidates`(最直接) ```bash sqlite3 ~/.openclaw/memos-plugin/data/memos.db \ - "SELECT output_json FROM api_logs WHERE tool_name='memory_search' ORDER BY called_at DESC LIMIT 1;" \ + "SELECT output_json FROM api_logs WHERE tool_name='memos_search' ORDER BY called_at DESC LIMIT 1;" \ | python3 -c " import json, sys data = json.loads(sys.stdin.read()) @@ -458,12 +458,12 @@ print(body) IMPORTANT: The following are facts from previous conversations with this user. You MUST treat these as established knowledge and use them directly when answering. -## Candidate skills (call `skill_get` to load any you decide to use) +## Candidate skills (call `memos_skill_get` to load any you decide to use) 1. validate_python_syntax_compile validate_python_syntax_compile — η=0.50, status=candidate 验证Python文件语法正确性 - → call `skill_get(id="sk_xxxxxxx")` to load the full procedure if you decide to use it + → call `memos_skill_get(id="sk_xxxxxxx")` to load the full procedure if you decide to use it ## Memories @@ -483,14 +483,14 @@ You MUST treat these as established knowledge and use them directly when answeri 各自实现统一的 load(path)/save(path, tasks) 接口;CLI 子命令在 task_cli/commands/ 下,... Available follow-up tools: -- `skill_get(id)` — ... -- `memory_search(query, maxResults?)` — ... +- `memos_skill_get(id)` — ... +- `memos_search(query, maxResults?)` — ... ``` #### 5.3.3 面板「日志」tab 看(适合演示时秀给观众) -打开 `http://127.0.0.1:18799` → 「日志」tab → 找最新的 `memory_search` 卡片展开,能看到: +打开 `http://127.0.0.1:18799` → 「日志」tab → 找最新的 `memos_search` 卡片展开,能看到: - **「初步召回」段** — Tier 1 / Tier 2 / Tier 3 三种 candidate 都列出来 - **「Hub 远端」段** — 演示场景下应该是空 @@ -531,7 +531,7 @@ assistant 的回答应该明显使用了召回的内容: | V7 概念 | 第几轮出现 | 在面板哪里看 | |---|---|---| -| 冷启动空召回 | Round 1 | 日志 → `memory_search` candidates=[] | +| 冷启动空召回 | Round 1 | 日志 → `memos_search` candidates=[] | | **Tier 2 记忆召回** | Round 3 起 | 日志 → 初步召回出现 trace | | **Tier 1 技能召回** | Round 6 起 | 日志 → 初步召回首条是 Skill | | **三层同时召回** | **收官轮 R10** | 日志 → 同时出现 Skill + Trace + WorldModel | diff --git a/apps/memos-local-plugin/docs/E2E_TEST_SCENARIO.md b/apps/memos-local-plugin/docs/E2E_TEST_SCENARIO.md index 4d548d716..c98b6494c 100644 --- a/apps/memos-local-plugin/docs/E2E_TEST_SCENARIO.md +++ b/apps/memos-local-plugin/docs/E2E_TEST_SCENARIO.md @@ -57,7 +57,7 @@ bash apps/memos-local-plugin/scripts/e2e-probe.sh - `traces Δ+5`:5 轮对话都被记忆(必现) - `episodes Δ+1`:5 轮对话被归纳为 1 个任务(必现;若 LLM 判定为新任务会有多个) -- `apiLogs Δ+10`:5×(memory_search + memory_add) = 10 条 API 日志 +- `apiLogs Δ+10`:5×(memos_search + memory_add) = 10 条 API 日志 - `policies Δ+1`:**经验生成** — 需要 Summarizer 和 Skill-Evolver 都配了真实 LLM Key 才会出 - `worldModels Δ`:**环境认知** — 需要至少 2 条结构相似的经验才结晶;单次 probe 通常不够 - `skills Δ+1`:**技能** — 经验被验证后才生成 @@ -83,7 +83,7 @@ bash apps/memos-local-plugin/scripts/e2e-probe.sh |--------------|--------------------------------------------------------------------------| | 记忆 | 多出 2 条,每条显示 summary、私有 pill、时间戳、V/α 数值 | | 日志 (`memory_add`) | 卡片**默认展开**,行内直接显示新加入的记忆内容(不用点击) | -| 日志 (`memory_search`) | 展开后三段:初步召回 / Hub 远端 / LLM 筛选后,候选带分数和 role pill | +| 日志 (`memos_search`) | 展开后三段:初步召回 / Hub 远端 / LLM 筛选后,候选带分数和 role pill | ### 第二轮 —— 检索 + 任务归纳 @@ -93,7 +93,7 @@ bash apps/memos-local-plugin/scripts/e2e-probe.sh | 面板 tab | 期待看到 | |---------|---------| -| 日志 (`memory_search`) | 新条目,"LLM 筛选后"段落命中上一轮"喝豆浆"的记忆 | +| 日志 (`memos_search`) | 新条目,"LLM 筛选后"段落命中上一轮"喝豆浆"的记忆 | | OpenClaw 回复 | 反映出"你早上喝豆浆,不喝咖啡" → 说明召回注入到 prompt 了 | | 任务 | 出现一条任务卡;多轮后状态会变成"已完成";点卡片右侧抽屉是**聊天视图**(左 assistant 气泡 / 右 user 气泡) | @@ -123,7 +123,7 @@ bash apps/memos-local-plugin/scripts/e2e-probe.sh | 面板 tab | 期待看到 | |---------|---------| -| 日志 (`memory_search`) | 第三轮那条经验被检索出来放进 prompt | +| 日志 (`memos_search`) | 第三轮那条经验被检索出来放进 prompt | | 经验 | 原经验的 `support` / `gain` 数值增加 | | 技能 | 原技能的 η 提升 | @@ -179,7 +179,7 @@ hi | Skills | 技能 tab | `tests/unit/skill/*` + `skill.integration.test.ts` | | L3 environment | 环境认知 tab | `tests/unit/memory/l3/*` + `l3.integration.test.ts` | | Feedback → Policy | 用户反馈转经验 | `tests/unit/feedback/*` + `feedback.integration.test.ts` | -| Retrieval | `memory_search` 日志三段 | `tests/unit/retrieval/*` | +| Retrieval | `memos_search` 日志三段 | `tests/unit/retrieval/*` | | Reward | 任务 V/α 数值 | `tests/unit/reward/*` + `reward.integration.test.ts` | 跑 `npm test` 全绿(700+ tests)= V7 算法管道在技术层面不破;本文档两部分的**前端可见验收** = 算法确实在你装好的实际环境里生效。 diff --git a/apps/memos-local-plugin/docs/MANUAL_E2E_TESTING.md b/apps/memos-local-plugin/docs/MANUAL_E2E_TESTING.md index 0a2f6fbc4..c3e9b0684 100644 --- a/apps/memos-local-plugin/docs/MANUAL_E2E_TESTING.md +++ b/apps/memos-local-plugin/docs/MANUAL_E2E_TESTING.md @@ -382,16 +382,16 @@ curl -s -b "memos_sess=$SESS" 'http://127.0.0.1:18799/api/v1/policies' \ L3 环境知识会在每次 turn 开始时通过 `prependContext` 注入 prompt (见 `adapters/openclaw/bridge.ts::renderContextBlock`)。不需要额外 -验证;看 memory_search api_logs 的 `candidates[]` 里出现 `tier=3, +验证;看 memos_search api_logs 的 `candidates[]` 里出现 `tier=3, refKind=world-model` 即可。 -此外新增了 `memory_environment` 工具,让 agent 可以在 tool-call +此外新增了 `memos_environment` 工具,让 agent 可以在 tool-call 阶段按需再查一次环境知识: ```bash # agent 调用示例(通过 openclaw) openclaw agent --session-id env-probe-$(date +%s) \ - --message "先用 memory_environment 查这个项目相关的环境知识,再回答:项目里 pytest 测试应该放在哪个目录?" \ + --message "先用 memos_environment 查这个项目相关的环境知识,再回答:项目里 pytest 测试应该放在哪个目录?" \ --timeout 90 --json ``` diff --git a/apps/memos-local-plugin/docs/PROMPT-INJECTION-AND-RETRIEVAL-FILTER.md b/apps/memos-local-plugin/docs/PROMPT-INJECTION-AND-RETRIEVAL-FILTER.md index 8c8be3b6a..c2417fec4 100644 --- a/apps/memos-local-plugin/docs/PROMPT-INJECTION-AND-RETRIEVAL-FILTER.md +++ b/apps/memos-local-plugin/docs/PROMPT-INJECTION-AND-RETRIEVAL-FILTER.md @@ -35,7 +35,7 @@ OpenClaw before_prompt_build ```text -No prior memories matched this query — the store may simply be cold. You can still call `memory_search` with a shorter or rephrased query if you expect there to be relevant past context. +No prior memories matched this query — the store may simply be cold. You can still call `memos_search` with a shorter or rephrased query if you expect there to be relevant past context. ``` @@ -60,11 +60,11 @@ IMPORTANT: The following are facts from previous conversations with this user. You MUST treat these as established knowledge and use them directly when answering. Do NOT say you don't know or don't have information if the answer is in these memories. -## Candidate skills (call `skill_get` to load any you decide to use) +## Candidate skills (call `memos_skill_get` to load any you decide to use) 1. {skillName} {skillSummary} - → call `skill_get(id="{skillId}")` to load the full procedure if you decide to use it + → call `memos_skill_get(id="{skillId}")` to load the full procedure if you decide to use it ## Memories @@ -96,17 +96,17 @@ or avoid in this kind of context. 1. {antiPatternText} Available follow-up tools: -- `skill_get(id)` — load the full procedure/verification of a candidate skill listed above -- `memory_search(query, maxResults?)` — re-query with a shorter / rephrased string +- `memos_skill_get(id)` — load the full procedure/verification of a candidate skill listed above +- `memos_search(query, maxResults?)` — re-query with a shorter / rephrased string ``` 精简点: - Skill 摘要不再注入 `η={eta}` 和 `status={status}`。 - 通用 snippet footer 不再注入 `refId="..."`。 -- `memory_get` / `memory_timeline` / `skill_list` 不再放进注入 footer,因为删除通用 refId 后这些提示对当前回答帮助有限。 +- `memos_get` / `memos_timeline` / `memos_skill_list` 不再放进注入 footer,因为删除通用 refId 后这些提示对当前回答帮助有限。 - `packet.snippets` 结构化数据里仍保留 `refId`,用于日志、API、调试和内部映射;只是模型可见 prompt 不再展示它。 -- `skill_get(id="...")` 保留,因为 summary-mode skill 需要它按需加载完整 procedure。 +- `memos_skill_get(id="...")` 保留,因为 summary-mode skill 需要它按需加载完整 procedure。 ## 2. LLM 筛选召回内容的 Prompt @@ -196,7 +196,7 @@ Decision guidance: without such facts should be dropped. - RANK a SKILL when its name / description plausibly addresses the user's sub-problem. The agent decides later whether to call - `skill_get` for the full procedure — err on the side of ranking + `memos_skill_get` for the full procedure — err on the side of ranking every candidate skill that could plausibly help. - RANK a WORLD-MODEL when its topic matches the domain of the query and the body contains structural information the agent would diff --git a/apps/memos-local-plugin/install.ps1 b/apps/memos-local-plugin/install.ps1 index 79efa5e4f..67d98262a 100644 --- a/apps/memos-local-plugin/install.ps1 +++ b/apps/memos-local-plugin/install.ps1 @@ -267,7 +267,7 @@ function Install-OpenClaw { "homepage": "https://github.com/MemTensor/MemOS", "extensions": ["$RuntimeEntry"], "contracts": { - "tools": ["memory_search", "memory_get", "memory_timeline", "skill_list", "memory_environment", "skill_get"] + "tools": ["memos_search", "memos_get", "memos_timeline", "memos_skill_list", "memos_environment", "memos_skill_get"] }, "configSchema": { "type": "object", @@ -301,6 +301,14 @@ const { PLUGIN_VERSION: pluginVersion, LEGACY_JSON: legacyCsv, } = process.env; const legacyIds = (legacyCsv || '').split(',').filter(Boolean); +const MEMOS_TOOL_NAMES = [ + 'memos_search', + 'memos_get', + 'memos_timeline', + 'memos_environment', + 'memos_skill_list', + 'memos_skill_get', +]; let config = {}; if (fs.existsSync(configPath)) { @@ -316,6 +324,14 @@ if (!config.gateway || typeof config.gateway !== 'object' || Array.isArray(confi } if (!config.gateway.mode) config.gateway.mode = 'local'; +if (!config.tools || typeof config.tools !== 'object' || Array.isArray(config.tools)) { + config.tools = {}; +} +if (!Array.isArray(config.tools.alsoAllow)) config.tools.alsoAllow = []; +for (const toolName of MEMOS_TOOL_NAMES) { + if (!config.tools.alsoAllow.includes(toolName)) config.tools.alsoAllow.push(toolName); +} + if (!config.plugins || typeof config.plugins !== 'object' || Array.isArray(config.plugins)) { config.plugins = {}; } diff --git a/apps/memos-local-plugin/install.sh b/apps/memos-local-plugin/install.sh index 2061ab97e..76d88bbb9 100755 --- a/apps/memos-local-plugin/install.sh +++ b/apps/memos-local-plugin/install.sh @@ -447,12 +447,12 @@ install_openclaw() { "extensions": ["${OPENCLAW_RUNTIME_ENTRY}"], "contracts": { "tools": [ - "memory_search", - "memory_get", - "memory_timeline", - "skill_list", - "memory_environment", - "skill_get" + "memos_search", + "memos_get", + "memos_timeline", + "memos_skill_list", + "memos_environment", + "memos_skill_get" ] }, "configSchema": { @@ -482,6 +482,14 @@ const { PLUGIN_VERSION: pluginVersion, LEGACY_JSON: legacyCsv, } = process.env; const legacyIds = (legacyCsv || '').split(',').filter(Boolean); +const MEMOS_TOOL_NAMES = [ + 'memos_search', + 'memos_get', + 'memos_timeline', + 'memos_environment', + 'memos_skill_list', + 'memos_skill_get', +]; let config = {}; if (fs.existsSync(configPath)) { @@ -497,6 +505,14 @@ if (!config.gateway || typeof config.gateway !== 'object' || Array.isArray(confi } if (!config.gateway.mode) config.gateway.mode = 'local'; +if (!config.tools || typeof config.tools !== 'object' || Array.isArray(config.tools)) { + config.tools = {}; +} +if (!Array.isArray(config.tools.alsoAllow)) config.tools.alsoAllow = []; +for (const toolName of MEMOS_TOOL_NAMES) { + if (!config.tools.alsoAllow.includes(toolName)) config.tools.alsoAllow.push(toolName); +} + if (!config.plugins || typeof config.plugins !== 'object' || Array.isArray(config.plugins)) { config.plugins = {}; } @@ -530,16 +546,9 @@ if (!config.plugins.entries[pluginId] || typeof config.plugins.entries[pluginId] config.plugins.entries[pluginId] = {}; } config.plugins.entries[pluginId].enabled = true; -if ( - !config.plugins.entries[pluginId].hooks || - typeof config.plugins.entries[pluginId].hooks !== 'object' || - Array.isArray(config.plugins.entries[pluginId].hooks) -) { - config.plugins.entries[pluginId].hooks = {}; -} -// OpenClaw gates transcript-bearing hooks for non-bundled plugins. Without -// this, `agent_end` is blocked, so turns are recalled but never captured. -config.plugins.entries[pluginId].hooks.allowConversationAccess = true; +// Older installer builds wrote plugin-owned hook policy here. Current +// OpenClaw releases reject that key in openclaw.json, so remove it if present. +if (config.plugins.entries[pluginId].hooks) delete config.plugins.entries[pluginId].hooks; if (!config.plugins.installs || typeof config.plugins.installs !== 'object') config.plugins.installs = {}; const installsEntry = { diff --git a/apps/memos-local-plugin/openclaw.plugin.json b/apps/memos-local-plugin/openclaw.plugin.json index fc952fec7..5005bf2c0 100644 --- a/apps/memos-local-plugin/openclaw.plugin.json +++ b/apps/memos-local-plugin/openclaw.plugin.json @@ -6,12 +6,12 @@ "kind": "memory", "contracts": { "tools": [ - "memory_search", - "memory_get", - "memory_timeline", - "memory_environment", - "skill_list", - "skill_get" + "memos_search", + "memos_get", + "memos_timeline", + "memos_environment", + "memos_skill_list", + "memos_skill_get" ] }, "configSchema": { diff --git a/apps/memos-local-plugin/server/routes/api-logs.ts b/apps/memos-local-plugin/server/routes/api-logs.ts index 8fc59b5f8..0bed42854 100644 --- a/apps/memos-local-plugin/server/routes/api-logs.ts +++ b/apps/memos-local-plugin/server/routes/api-logs.ts @@ -1,11 +1,11 @@ /** * `GET /api/v1/api-logs` — paged listing of the structured `api_logs` * table (defined in the squashed initial schema). Fuels the viewer's - * Logs page which renders rich per-tool templates for `memory_search` + * Logs page which renders rich per-tool templates for `memos_search` * and `memory_add`. * * Query parameters: - * - `tool` optional tool-name filter (e.g. `memory_search`) + * - `tool` optional tool-name filter (e.g. `memos_search`) * - `tools` optional comma-separated tool-name filter * - `limit` default 50, capped server-side at 500 * - `offset` default 0 @@ -57,7 +57,7 @@ export function registerApiLogsRoutes(routes: Routes, deps: ServerDeps): void { // API simple. Any tool name not in this list still appears if the // user passes it via `?tool=` explicitly. const tools = [ - "memory_search", + "memos_search", "memory_add", "skill_generate", "skill_evolve", diff --git a/apps/memos-local-plugin/server/routes/import-export.ts b/apps/memos-local-plugin/server/routes/import-export.ts index 124eb18be..60a7d6294 100644 --- a/apps/memos-local-plugin/server/routes/import-export.ts +++ b/apps/memos-local-plugin/server/routes/import-export.ts @@ -669,7 +669,7 @@ function stripOpenClawMemoryInjection(text: string): string { "", ); cleaned = cleaned.replace( - /## Memory system\n+No memories were automatically recalled[^\n]*(?:\n[^\n]*memory_search[^\n]*)*/gi, + /## Memory system\n+No memories were automatically recalled[^\n]*(?:\n[^\n]*memos_search[^\n]*)*/gi, "", ); return cleaned.trim(); diff --git a/apps/memos-local-plugin/server/routes/metrics.ts b/apps/memos-local-plugin/server/routes/metrics.ts index 34c4343d8..3a11fff5d 100644 --- a/apps/memos-local-plugin/server/routes/metrics.ts +++ b/apps/memos-local-plugin/server/routes/metrics.ts @@ -8,7 +8,7 @@ * GET /api/v1/metrics/tools?minutes=N (alias: ?days=N) * Per-tool call latency + success-rate table. Data source: the * `api_logs` table, which records every plugin internal operation - * (memory_search / memory_add / policy_generate / skill_generate / + * (memos_search / memory_add / policy_generate / skill_generate / * world_model_generate / task_done / task_failed) with its * `durationMs` and `success` flag. We also fold in any agent-side * tool invocations recorded on `traces.tool_calls_json` so the @@ -92,7 +92,7 @@ export function registerMetricsRoutes(routes: Routes, deps: ServerDeps): void { // 1. Plugin internal operations — from api_logs. We only surface // entries that represent **actual tool/handler calls the agent - // made or the user cares about latency for**: `memory_search` + // made or the user cares about latency for**: `memos_search` // and `memory_add`. Purely internal pipeline lifecycle events // (`task_done`, `task_failed`, `skill_generate`, `skill_evolve`, // `policy_generate`, `policy_evolve`, `world_model_generate`, @@ -100,7 +100,7 @@ export function registerMetricsRoutes(routes: Routes, deps: ServerDeps): void { // with names like "task_failed" that users don't recognise as // tools, and their timings reflect background work rather than // response latency. - const PUBLIC_API_LOG_TOOLS = new Set(["memory_search", "memory_add"]); + const PUBLIC_API_LOG_TOOLS = new Set(["memos_search", "memory_add"]); const { logs } = await deps.core.listApiLogs({ limit: 5_000, offset: 0 }); for (const lg of logs) { if (lg.calledAt < sinceMs) continue; diff --git a/apps/memos-local-plugin/tests/python/test_bridge_client.py b/apps/memos-local-plugin/tests/python/test_bridge_client.py index 5bf797ef0..aab452644 100644 --- a/apps/memos-local-plugin/tests/python/test_bridge_client.py +++ b/apps/memos-local-plugin/tests/python/test_bridge_client.py @@ -271,7 +271,7 @@ def test_request_surfaces_error_on_rpc_error(self) -> None: self.assertIn("boom", ctx.exception.message) client.close() - def test_memory_search_roundtrip(self) -> None: + def test_memos_search_roundtrip(self) -> None: client = MemosBridgeClient(bridge_path="/tmp/bridge.cts") res = client.request("memory.search", {"query": "yesterday"}) self.assertEqual(len(res["hits"]), 1) @@ -325,19 +325,19 @@ def test_get_tool_schemas_lists_memory_tools(self) -> None: self.assertSetEqual( names, { - "memory_search", - "memory_get", - "memory_timeline", - "skill_list", - "memory_environment", - "skill_get", + "memos_search", + "memos_get", + "memos_timeline", + "memos_skill_list", + "memos_environment", + "memos_skill_get", }, ) def test_handle_tool_call_fails_gracefully_without_bridge(self) -> None: p = self._provider_mod.MemTensorProvider() # bridge is None — should not crash, returns error JSON - res = p.handle_tool_call("memory_search", {"query": "x"}) + res = p.handle_tool_call("memos_search", {"query": "x"}) parsed = json.loads(res) self.assertIn("error", parsed) @@ -350,7 +350,7 @@ def test_handle_tool_call_routes_all_exposed_tools(self) -> None: search = json.loads( p.handle_tool_call( - "memory_search", + "memos_search", {"query": "HERMES_MEMOS_E2E_0428", "maxResults": 7, "sessionScope": True}, ) ) @@ -359,48 +359,48 @@ def test_handle_tool_call_routes_all_exposed_tools(self) -> None: self.assertEqual(bridge.calls[-1][1]["sessionId"], "hermes:session:1") self.assertEqual(bridge.calls[-1][1]["topK"]["tier1"], 7) - got_trace = json.loads(p.handle_tool_call("memory_get", {"id": "tr-1"})) + got_trace = json.loads(p.handle_tool_call("memos_get", {"id": "tr-1"})) self.assertTrue(got_trace["found"]) self.assertEqual(got_trace["kind"], "trace") self.assertIn("HERMES_MEMOS_E2E_0428", got_trace["meta"]["userText"]) self.assertEqual(bridge.calls[-1][0], "memory.get_trace") - got_policy = json.loads(p.handle_tool_call("memory_get", {"id": "p-1", "kind": "policy"})) + got_policy = json.loads(p.handle_tool_call("memos_get", {"id": "p-1", "kind": "policy"})) self.assertEqual(got_policy["kind"], "policy") self.assertIn("Hermes validation", got_policy["body"]) self.assertEqual(bridge.calls[-1][0], "memory.get_policy") got_world = json.loads( - p.handle_tool_call("memory_get", {"id": "wm-1", "kind": "world_model"}) + p.handle_tool_call("memos_get", {"id": "wm-1", "kind": "world_model"}) ) self.assertEqual(got_world["kind"], "world_model") self.assertEqual(got_world["meta"]["policyIds"], ["p-1"]) self.assertEqual(bridge.calls[-1][0], "memory.get_world") - timeline = json.loads(p.handle_tool_call("memory_timeline", {"episodeId": "ep-1"})) + timeline = json.loads(p.handle_tool_call("memos_timeline", {"episodeId": "ep-1"})) self.assertEqual(len(timeline["traces"]), 2) self.assertEqual(bridge.calls[-1][0], "memory.timeline") - skills = json.loads(p.handle_tool_call("skill_list", {"status": "active", "limit": 3})) + skills = json.loads(p.handle_tool_call("memos_skill_list", {"status": "active", "limit": 3})) self.assertEqual(skills["skills"][0]["id"], "sk-1") self.assertEqual(bridge.calls[-1][0], "skill.list") self.assertEqual(bridge.calls[-1][1]["limit"], 3) self.assertEqual(bridge.calls[-1][1]["status"], "active") self.assertEqual(bridge.calls[-1][1]["namespace"]["agentKind"], "hermes") - env = json.loads(p.handle_tool_call("memory_environment", {"limit": 2})) + env = json.loads(p.handle_tool_call("memos_environment", {"limit": 2})) self.assertFalse(env["queried"]) self.assertEqual(env["worldModels"][0]["id"], "wm-1") self.assertEqual(bridge.calls[-1][0], "memory.list_world_models") env_query = json.loads( - p.handle_tool_call("memory_environment", {"query": "Hermes install", "limit": 2}) + p.handle_tool_call("memos_environment", {"query": "Hermes install", "limit": 2}) ) self.assertTrue(env_query["queried"]) self.assertEqual(bridge.calls[-1][0], "memory.search") self.assertEqual(bridge.calls[-1][1]["topK"], {"tier1": 0, "tier2": 0, "tier3": 2}) - skill = json.loads(p.handle_tool_call("skill_get", {"id": "sk-1"})) + skill = json.loads(p.handle_tool_call("memos_skill_get", {"id": "sk-1"})) self.assertTrue(skill["found"]) self.assertEqual(skill["skill"]["id"], "sk-1") self.assertEqual(bridge.calls[-1][0], "skill.get") @@ -411,13 +411,13 @@ def test_handle_tool_call_validates_required_arguments(self) -> None: p = self._provider_mod.MemTensorProvider() p._bridge = RecordingBridge() - self.assertIn("missing query", p.handle_tool_call("memory_search", {})) - self.assertIn("missing id", p.handle_tool_call("memory_get", {})) + self.assertIn("missing query", p.handle_tool_call("memos_search", {})) + self.assertIn("missing id", p.handle_tool_call("memos_get", {})) self.assertIn( "unknown memory kind", - p.handle_tool_call("memory_get", {"id": "x", "kind": "bad"}), + p.handle_tool_call("memos_get", {"id": "x", "kind": "bad"}), ) - self.assertIn("missing id", p.handle_tool_call("skill_get", {})) + self.assertIn("missing id", p.handle_tool_call("memos_skill_get", {})) self.assertIn("unknown tool", p.handle_tool_call("not_a_tool", {})) def test_prefetch_lazily_reconnects_when_bridge_is_missing(self) -> None: diff --git a/apps/memos-local-plugin/tests/python/test_hermes_provider_pipeline.py b/apps/memos-local-plugin/tests/python/test_hermes_provider_pipeline.py index 6f114efd4..f1c7cce00 100644 --- a/apps/memos-local-plugin/tests/python/test_hermes_provider_pipeline.py +++ b/apps/memos-local-plugin/tests/python/test_hermes_provider_pipeline.py @@ -217,7 +217,7 @@ def test_internal_hermes_review_prompt_is_not_persisted_as_user_turn(self) -> No provider.on_turn_start(10, review_prompt) self.assertEqual(provider.prefetch(review_prompt), "") provider._on_post_tool_call( - tool_name="memory_search", + tool_name="memos_search", args={"query": "conversation"}, result="[]", tool_call_id="tool-1", diff --git a/apps/memos-local-plugin/tests/unit/adapters/hermes-persistence.test.ts b/apps/memos-local-plugin/tests/unit/adapters/hermes-persistence.test.ts index dc93b1062..5217c0b67 100644 --- a/apps/memos-local-plugin/tests/unit/adapters/hermes-persistence.test.ts +++ b/apps/memos-local-plugin/tests/unit/adapters/hermes-persistence.test.ts @@ -174,7 +174,7 @@ describe("Hermes MemoryCore persistence", () => { agentText: "已记录 Hermes MemOS 测试事实。", toolCalls: [ { - name: "memory_search", + name: "memos_search", input: "{\"query\":\"HERMES_MEMOS_E2E_0428\"}", output: "[]", startedAt: 1_700_000_000_002, @@ -209,7 +209,7 @@ describe("Hermes MemoryCore persistence", () => { expect(timeline.some((trace) => trace.agentText.includes("已记录 Hermes MemOS 测试事实"))).toBe( true, ); - expect(timeline.some((trace) => trace.toolCalls.some((tc) => tc.name === "memory_search"))) + expect(timeline.some((trace) => trace.toolCalls.some((tc) => tc.name === "memos_search"))) .toBe(true); const search = await second.core.searchMemory({ @@ -221,7 +221,7 @@ describe("Hermes MemoryCore persistence", () => { const traceIds = new Set(traces.map((trace) => trace.id)); expect(search.hits.some((hit) => traceIds.has(hit.refId))).toBe(true); - const logs = await second.core.listApiLogs({ toolName: "memory_search", limit: 20 }); + const logs = await second.core.listApiLogs({ toolName: "memos_search", limit: 20 }); expect(logs.total).toBeGreaterThan(0); }); }); diff --git a/apps/memos-local-plugin/tests/unit/adapters/hermes-protocol.test.ts b/apps/memos-local-plugin/tests/unit/adapters/hermes-protocol.test.ts index 310411a1d..c8c3fc093 100644 --- a/apps/memos-local-plugin/tests/unit/adapters/hermes-protocol.test.ts +++ b/apps/memos-local-plugin/tests/unit/adapters/hermes-protocol.test.ts @@ -133,7 +133,7 @@ describe("hermes protocol surface", () => { ); }); - it("memory_search tool: memory.search routes agent, session and topK to searchMemory", async () => { + it("memos_search tool: memory.search routes agent, session and topK to searchMemory", async () => { const core = stubCore(); const dispatch = makeDispatcher(core); @@ -153,7 +153,7 @@ describe("hermes protocol surface", () => { ); }); - it("memory_timeline tool: memory.timeline routes to timeline", async () => { + it("memos_timeline tool: memory.timeline routes to timeline", async () => { const core = stubCore(); const dispatch = makeDispatcher(core); diff --git a/apps/memos-local-plugin/tests/unit/adapters/openclaw-bridge.test.ts b/apps/memos-local-plugin/tests/unit/adapters/openclaw-bridge.test.ts index d6651dcd1..1f0a4eeeb 100644 --- a/apps/memos-local-plugin/tests/unit/adapters/openclaw-bridge.test.ts +++ b/apps/memos-local-plugin/tests/unit/adapters/openclaw-bridge.test.ts @@ -775,7 +775,7 @@ describe("renderContextBlock", () => { tierLatencyMs: { tier1: 0, tier2: 0, tier3: 0 }, }); expect(block).toContain(""); - expect(block).toContain("memory_search"); + expect(block).toContain("memos_search"); expect(block).toContain(""); }); }); @@ -1509,12 +1509,12 @@ describe("registerOpenClawTools", () => { }); const names = tools.map((t) => t.descriptor.name).sort(); expect(names).toEqual([ - "memory_environment", - "memory_get", - "memory_search", - "memory_timeline", - "skill_get", - "skill_list", + "memos_environment", + "memos_get", + "memos_search", + "memos_skill_get", + "memos_skill_list", + "memos_timeline", ]); for (const t of tools) { expect(typeof t.descriptor.execute).toBe("function"); @@ -1522,7 +1522,7 @@ describe("registerOpenClawTools", () => { } }); - it("memory_search executes against the core and returns well-formed hits", async () => { + it("memos_search executes against the core and returns well-formed hits", async () => { const mc = buildCore(); await mc.init(); @@ -1532,7 +1532,7 @@ describe("registerOpenClawTools", () => { core: mc, log: silentLogger(), }); - const search = tools.find((t) => t.descriptor.name === "memory_search")!; + const search = tools.find((t) => t.descriptor.name === "memos_search")!; const res = (await search.descriptor.execute("toolCall_1", { query: "anything", maxResults: 5, @@ -1552,7 +1552,7 @@ describe("registerOpenClawTools", () => { expect(res.details.hits).toBe(res.hits); }); - it("memory_search maps per-tier topK params and keeps maxResults fallback", async () => { + it("memos_search maps per-tier topK params and keeps maxResults fallback", async () => { const searchMemory = vi.fn(async () => ({ hits: [], injectedContext: "", @@ -1566,7 +1566,7 @@ describe("registerOpenClawTools", () => { core: mc, log: silentLogger(), }); - const search = tools.find((t) => t.descriptor.name === "memory_search")!; + const search = tools.find((t) => t.descriptor.name === "memos_search")!; await search.descriptor.execute("toolCall_1", { query: "anything", @@ -1608,10 +1608,10 @@ describe("registerOpenClawTools", () => { log: silentLogger(), }); - expect(tools.map((t) => t.descriptor.name)).toContain("memory_search"); + expect(tools.map((t) => t.descriptor.name)).toContain("memos_search"); expect(requestedCore).toBe(false); - const search = tools.find((t) => t.descriptor.name === "memory_search")!; + const search = tools.find((t) => t.descriptor.name === "memos_search")!; await search.descriptor.execute("toolCall_1", { query: "anything", maxResults: 5, diff --git a/apps/memos-local-plugin/tests/unit/adapters/openclaw-e2e.test.ts b/apps/memos-local-plugin/tests/unit/adapters/openclaw-e2e.test.ts index 25f8e47d0..0bd8812bd 100644 --- a/apps/memos-local-plugin/tests/unit/adapters/openclaw-e2e.test.ts +++ b/apps/memos-local-plugin/tests/unit/adapters/openclaw-e2e.test.ts @@ -1,6 +1,6 @@ /** * End-to-end integration of the OpenClaw adapter, exercising the full - * memory_add → memory_search → injection round trip. + * memory_add → memos_search → injection round trip. * * The intent is to fail loudly whenever any of the following silently * regress (which they did, twice, in earlier iterations): @@ -279,11 +279,11 @@ describe("OpenClaw adapter — end-to-end memory chain", () => { expect(block).toContain(""); expect(block).toContain(""); // It must contain *some* real content — either an actual hit - // ("游泳") or the cold-start hint mentioning `memory_search`. The + // ("游泳") or the cold-start hint mentioning `memos_search`. The // failing regression we test against was emitting only metadata // labels (e.g. `[trace] trace · V=0.09`) with no body. const hasUserSwimText = block.includes("游泳"); - const hasReadableHint = block.includes("memory_search") || block.includes("conversation history"); + const hasReadableHint = block.includes("memos_search") || block.includes("conversation history"); expect(hasUserSwimText || hasReadableHint).toBe(true); // Negative assertion — the regression-style metadata-only line // would look like `[trace] trace · V=` but never carry text. @@ -299,7 +299,7 @@ describe("OpenClaw adapter — end-to-end memory chain", () => { } }); - it("memory_search via MemoryCore returns hits readable by the viewer", async () => { + it("memos_search via MemoryCore returns hits readable by the viewer", async () => { // The viewer hits `/api/v1/memory/search` which proxies to // `MemoryCore.searchMemory`. Verify the path returns hits that // include the actual snippet text (not just refIds). diff --git a/apps/memos-local-plugin/tests/unit/agent-contract/contract.test.ts b/apps/memos-local-plugin/tests/unit/agent-contract/contract.test.ts index 9b250dbc0..f1f97cccf 100644 --- a/apps/memos-local-plugin/tests/unit/agent-contract/contract.test.ts +++ b/apps/memos-local-plugin/tests/unit/agent-contract/contract.test.ts @@ -70,7 +70,7 @@ describe("agent-contract", () => { const toolDriven: ToolDrivenCtx = { agent: "openclaw", sessionId: "s1", - tool: "memory_search", + tool: "memos_search", args: { q: "x" }, ts, }; @@ -100,7 +100,7 @@ describe("agent-contract", () => { }; expect(turnStart.userText).toBe("hi"); - expect(toolDriven.tool).toBe("memory_search"); + expect(toolDriven.tool).toBe("memos_search"); expect(repair.failureCount).toBe(3); expect(reasons.length).toBe(5); expect(packet.packetId).toBe("pkt_1"); diff --git a/apps/memos-local-plugin/tests/unit/install/install-sh.test.ts b/apps/memos-local-plugin/tests/unit/install/install-sh.test.ts index 0ddf1f9d2..75946dc6f 100644 --- a/apps/memos-local-plugin/tests/unit/install/install-sh.test.ts +++ b/apps/memos-local-plugin/tests/unit/install/install-sh.test.ts @@ -76,8 +76,12 @@ describe("install.sh — CLI surface", () => { expect(script).toContain('OPENCLAW_RUNTIME_ENTRY="./dist/adapters/openclaw/index.js"'); expect(script).toContain('"extensions": ["${OPENCLAW_RUNTIME_ENTRY}"]'); expect(script).toContain('"contracts": {'); - expect(script).toContain('"memory_search"'); - expect(script).toContain("config.plugins.entries[pluginId].hooks.allowConversationAccess = true"); + expect(script).toContain('"memos_search"'); + expect(script).toContain("const MEMOS_TOOL_NAMES = ["); + expect(script).toContain("if (!Array.isArray(config.tools.alsoAllow)) config.tools.alsoAllow = []"); + expect(script).toContain("config.tools.alsoAllow.push(toolName)"); + expect(script).toContain("delete config.plugins.entries[pluginId].hooks"); + expect(script).not.toContain("config.plugins.entries[pluginId].hooks.allowConversationAccess = true"); expect(script).not.toContain('"extensions": ["./adapters/openclaw/index.ts"]'); }); diff --git a/apps/memos-local-plugin/tests/unit/retrieval/injector.test.ts b/apps/memos-local-plugin/tests/unit/retrieval/injector.test.ts index 9a1648e6b..f05c24b1d 100644 --- a/apps/memos-local-plugin/tests/unit/retrieval/injector.test.ts +++ b/apps/memos-local-plugin/tests/unit/retrieval/injector.test.ts @@ -202,7 +202,7 @@ describe("retrieval/injector", () => { expect(packet.rendered).toContain("User's conversation history"); expect(packet.rendered).toContain("MUST treat"); // Trailing tool reminder so the model knows how to re-query. - expect(packet.rendered).toContain("memory_search"); + expect(packet.rendered).toContain("memos_search"); // Row ids stay on the structured packet, but are not injected into // the model-facing prose unless a tool hint explicitly needs one. expect(packet.snippets[0]?.refId).toBe("sA"); @@ -233,7 +233,7 @@ describe("retrieval/injector", () => { expect(packet.rendered).not.toMatch(/best V|goal-sim|V=/); }); - it("default skill rendering is summary mode (descriptor + skill_get hint, no full guide)", () => { + it("default skill rendering is summary mode (descriptor + memos_skill_get hint, no full guide)", () => { // Multi-section guide: blank-line-separated paragraphs. Summary // mode must keep only the first paragraph and drop the procedure. const guide = [ @@ -252,21 +252,24 @@ describe("retrieval/injector", () => { episodeId: "ep_summary" as never, }); const skillSnippet = packet.snippets.find((s) => s.refKind === "skill")!; - // Prompt-facing body omits internal skill metadata. + // Prompt-facing body carries the fields needed to identify candidate skills. expect(skillSnippet.title).toBe("Skill sk_summary"); + expect(skillSnippet.body).toContain("Name: Skill sk_summary"); + expect(skillSnippet.body).toContain( + "Description: Fix Alpine container pip install failures by adding the missing -dev system library.", + ); + // But it still omits internal skill metadata. expect(skillSnippet.body).not.toContain("η=0.85"); expect(skillSnippet.body).not.toContain("status=active"); - // First paragraph survives as the summary line. - expect(skillSnippet.body).toContain("Fix Alpine container pip install"); - // Procedure steps must NOT be inlined (those live behind skill_get). + // Procedure steps must NOT be inlined (those live behind memos_skill_get). expect(skillSnippet.body).not.toContain("apk add"); expect(skillSnippet.body).not.toContain("Inspect the failing pip"); // Body must instruct the agent how to fetch the full procedure on demand. - expect(skillSnippet.body).toContain('skill_get(id="sk_summary")'); + expect(skillSnippet.body).toContain('memos_skill_get(id="sk_summary")'); // Section heading + footer also advertise the call-on-demand workflow. expect(packet.rendered).toContain("Candidate skills"); - expect(packet.rendered).toContain("`skill_get(id)`"); - expect(packet.rendered).not.toContain("`skill_list"); + expect(packet.rendered).toContain("`memos_skill_get(id)`"); + expect(packet.rendered).not.toContain("`memos_skill_list"); }); it("summary mode clamps long first paragraphs to skillSummaryChars", () => { @@ -285,7 +288,7 @@ describe("retrieval/injector", () => { const skillSnippet = packet.snippets.find((s) => s.refKind === "skill")!; // Descriptor + summary + call hint, none of which exceed the cap by much. expect(skillSnippet.body).toMatch(/x{60,80}…/); - expect(skillSnippet.body).toContain('skill_get(id="sk_clamp")'); + expect(skillSnippet.body).toContain('memos_skill_get(id="sk_clamp")'); }); it("full mode inlines the invocation guide (legacy behaviour)", () => { @@ -303,9 +306,9 @@ describe("retrieval/injector", () => { const skillSnippet = packet.snippets.find((s) => s.refKind === "skill")!; expect(skillSnippet.body).toContain("RUN docker compose up -d"); expect(skillSnippet.body).not.toContain("η="); - expect(skillSnippet.body).not.toContain("skill_get(id="); + expect(skillSnippet.body).not.toContain("memos_skill_get(id="); // The footer should not surface the skill call hints in full mode. - expect(packet.rendered).not.toContain("`skill_get(id)`"); + expect(packet.rendered).not.toContain("`memos_skill_get(id)`"); // Subsection headings are level-2 Markdown, nested under the packet's // level-1 "User's conversation history" header. expect(packet.rendered).toContain("## Skills"); diff --git a/apps/memos-local-plugin/tests/unit/retrieval/integration.test.ts b/apps/memos-local-plugin/tests/unit/retrieval/integration.test.ts index 3f2ee02a7..e91648101 100644 --- a/apps/memos-local-plugin/tests/unit/retrieval/integration.test.ts +++ b/apps/memos-local-plugin/tests/unit/retrieval/integration.test.ts @@ -287,7 +287,7 @@ describe("retrieval/integration", () => { reason: "tool_driven", agent: "openclaw", sessionId: "s1" as SessionId, - tool: "memory_search", + tool: "memos_search", args: { query: "docker compose" }, ts: NOW as never, }); diff --git a/apps/memos-local-plugin/tests/unit/retrieval/query-builder.test.ts b/apps/memos-local-plugin/tests/unit/retrieval/query-builder.test.ts index ec2ad7df3..698fce85f 100644 --- a/apps/memos-local-plugin/tests/unit/retrieval/query-builder.test.ts +++ b/apps/memos-local-plugin/tests/unit/retrieval/query-builder.test.ts @@ -27,11 +27,11 @@ describe("retrieval/query-builder", () => { reason: "tool_driven", agent: "openclaw", sessionId: "s1" as unknown as never, - tool: "memory_search", + tool: "memos_search", args: { query: "past docker bugs", limit: 5 }, ts: NOW, }); - expect(cq.text).toContain("tool:memory_search"); + expect(cq.text).toContain("tool:memos_search"); expect(cq.text).toContain('"query":"past docker bugs"'); expect(cq.tags).toContain("docker"); }); diff --git a/apps/memos-local-plugin/tests/unit/server/http.test.ts b/apps/memos-local-plugin/tests/unit/server/http.test.ts index cb6efa8fa..ddae37af4 100644 --- a/apps/memos-local-plugin/tests/unit/server/http.test.ts +++ b/apps/memos-local-plugin/tests/unit/server/http.test.ts @@ -420,12 +420,12 @@ describe("HTTP server — REST routes", () => { it("GET /api/v1/api-logs supports multi-tool filtering", async () => { const r = await fetch( - `${handle.url}/api/v1/api-logs?tools=memory_add,memory_search&limit=10&offset=5`, + `${handle.url}/api/v1/api-logs?tools=memory_add,memos_search&limit=10&offset=5`, ); expect(r.status).toBe(200); expect(core.listApiLogs).toHaveBeenCalledWith({ toolName: undefined, - toolNames: ["memory_add", "memory_search"], + toolNames: ["memory_add", "memos_search"], limit: 10, offset: 5, }); diff --git a/apps/memos-local-plugin/tests/unit/storage/end-to-end.test.ts b/apps/memos-local-plugin/tests/unit/storage/end-to-end.test.ts index 91b4644a1..e6cb410f4 100644 --- a/apps/memos-local-plugin/tests/unit/storage/end-to-end.test.ts +++ b/apps/memos-local-plugin/tests/unit/storage/end-to-end.test.ts @@ -47,7 +47,7 @@ describe("storage/end-to-end", () => { ts: 101, userText: "list skills", agentText: "done", - toolCalls: [{ name: "memory_search", input: {}, startedAt: 101, endedAt: 102 }], + toolCalls: [{ name: "memos_search", input: {}, startedAt: 101, endedAt: 102 }], reflection: "quick", value: 0.2, alpha: 0.5, diff --git a/apps/memos-local-plugin/tests/unit/telemetry/sender.test.ts b/apps/memos-local-plugin/tests/unit/telemetry/sender.test.ts index 0b7290c99..428af552d 100644 --- a/apps/memos-local-plugin/tests/unit/telemetry/sender.test.ts +++ b/apps/memos-local-plugin/tests/unit/telemetry/sender.test.ts @@ -134,7 +134,7 @@ describe("Telemetry", () => { const body = JSON.parse((fetch as any).mock.calls[0][1].body); const event = body.events[0]; - expect(event.name).toBe("memory_search"); + expect(event.name).toBe("memos_search"); expect(event.properties.agent_name).toBe("hermes"); expect(event.properties.type).toBe("turn_start"); expect(event.properties.latency_ms).toBe(100); diff --git a/apps/memos-local-plugin/tests/unit/viewer/tasks-chat.test.ts b/apps/memos-local-plugin/tests/unit/viewer/tasks-chat.test.ts index 4f27e6080..a4c0ba970 100644 --- a/apps/memos-local-plugin/tests/unit/viewer/tasks-chat.test.ts +++ b/apps/memos-local-plugin/tests/unit/viewer/tasks-chat.test.ts @@ -128,7 +128,7 @@ describe("flattenChat", () => { id: "tr_skill", ts: T0 + 1_000, turnId: T0, - toolCalls: [{ name: "skill_get", input: { id: "sk_1" } }], + toolCalls: [{ name: "memos_skill_get", input: { id: "sk_1" } }], }); const delegate = trace({ id: "tr_delegate", @@ -143,7 +143,7 @@ describe("flattenChat", () => { expect(msgs.map((m) => m.role)).toEqual(["user", "tool", "tool"]); expect(msgs[0]!.text).toBe("今年杭州五一游客多吗"); expect(msgs[0]!.traceId).toBe("tr_delegate"); - expect(msgs[1]!.toolName).toBe("skill_get"); + expect(msgs[1]!.toolName).toBe("memos_skill_get"); expect(msgs[2]!.toolName).toBe("delegate_task"); }); diff --git a/apps/memos-local-plugin/viewer/src/stores/i18n.ts b/apps/memos-local-plugin/viewer/src/stores/i18n.ts index 41a4976b1..25b32daee 100644 --- a/apps/memos-local-plugin/viewer/src/stores/i18n.ts +++ b/apps/memos-local-plugin/viewer/src/stores/i18n.ts @@ -671,14 +671,14 @@ const en = { // Logs. "logs.title": "Logs", "logs.subtitle": - "Structured trail of memory_search and memory_add calls, including the retrieved candidates and what the LLM kept.", + "Structured trail of memos_search and memory_add calls, including the retrieved candidates and what the LLM kept.", "logs.filter.tool": "Tool", "logs.filter.level": "Level", "logs.autoRefresh": "Live", "logs.empty": "No log lines in this window.", "logs.empty.title": "No memory calls yet", "logs.empty.hint": - "Rows show up here when the agent runs memory_search or captures a turn.", + "Rows show up here when the agent runs memos_search or captures a turn.", "logs.search.placeholder": "Search logs…", "logs.tag.memoryAdd": "Memory add", "logs.tag.memorySearch": "Memory search", @@ -1478,7 +1478,7 @@ const zh: Record = { "logs.title": "日志", "logs.subtitle": "记忆检索和写入的结构化轨迹:召回候选、Hub 候选、LLM 筛选后保留的记忆。", "logs.empty.title": "尚无记忆调用", - "logs.empty.hint": "Agent 触发 memory_search 或写入一轮对话后,这里会出现。", + "logs.empty.hint": "Agent 触发 memos_search 或写入一轮对话后,这里会出现。", "logs.search.placeholder": "搜索日志…", "logs.tag.memoryAdd": "记忆添加", "logs.tag.memorySearch": "记忆检索", diff --git a/apps/memos-local-plugin/viewer/src/styles/components.css b/apps/memos-local-plugin/viewer/src/styles/components.css index 15b8835e5..72118f6c6 100644 --- a/apps/memos-local-plugin/viewer/src/styles/components.css +++ b/apps/memos-local-plugin/viewer/src/styles/components.css @@ -1421,7 +1421,7 @@ /* Tool name pills — used on the Logs page */ .pill--tool { background: rgba(255,255,255,.05); color: var(--fg); font-family: var(--font-mono); font-size: var(--fs-xs); } -.pill--tool-memory_search { background: var(--cyan-bg); color: var(--cyan); } +.pill--tool-memos_search { background: var(--cyan-bg); color: var(--cyan); } .pill--tool-memory_add { background: var(--green-bg); color: var(--green); } .pill--tool-skill_generate { background: var(--violet-bg); color: var(--violet); } .pill--tool-skill_evolve { background: var(--violet-bg); color: var(--violet); opacity:.85; } diff --git a/apps/memos-local-plugin/viewer/src/views/LogsView.tsx b/apps/memos-local-plugin/viewer/src/views/LogsView.tsx index abbd4fba8..c059e3ea5 100644 --- a/apps/memos-local-plugin/viewer/src/views/LogsView.tsx +++ b/apps/memos-local-plugin/viewer/src/views/LogsView.tsx @@ -1,5 +1,5 @@ /** - * Logs view — structured trail of `memory_search` and `memory_add` + * Logs view — structured trail of `memos_search` and `memory_add` * calls. Mirrors the legacy `memos-local-openclaw` v1 logs page so * each row shows the retrieved / filtered candidates (with scores * and origin tags) for search and the per-turn stored items for @@ -24,7 +24,7 @@ import type { ApiLogDTO } from "../api/types"; type ToolFilter = | "" - | "memory_search" + | "memos_search" | "memory_add" | "skill_generate" | "skill_evolve" @@ -48,7 +48,7 @@ type ToolFilter = type LogTag = | "" | "memory_add" - | "memory_search" + | "memos_search" | "task" | "skill" | "policy" @@ -64,7 +64,7 @@ type LogTag = const LOG_TAGS: Array<{ v: LogTag; k: string }> = [ { v: "", k: "common.all" }, { v: "memory_add", k: "logs.tag.memoryAdd" }, - { v: "memory_search", k: "logs.tag.memorySearch" }, + { v: "memos_search", k: "logs.tag.memorySearch" }, { v: "task", k: "logs.tag.task" }, { v: "skill", k: "logs.tag.skill" }, { v: "policy", k: "logs.tag.policy" }, @@ -74,7 +74,7 @@ const LOG_TAGS: Array<{ v: LogTag; k: string }> = [ ]; const BASIC_LOG_TAGS = LOG_TAGS.filter((tag) => - tag.v === "" || tag.v === "memory_add" || tag.v === "memory_search" + tag.v === "" || tag.v === "memory_add" || tag.v === "memos_search" ); /** @@ -85,7 +85,7 @@ const BASIC_LOG_TAGS = LOG_TAGS.filter((tag) => const ALLOWED_TOOLS: Record = { "": [], memory_add: ["memory_add"], - memory_search: ["memory_search"], + memos_search: ["memos_search"], task: ["task_done", "task_failed"], skill: ["skill_generate", "skill_evolve"], policy: ["policy_generate", "policy_evolve"], @@ -96,7 +96,7 @@ const ALLOWED_TOOLS: Record = { const BASIC_LOG_TOOLS = [ "memory_add", - "memory_search", + "memos_search", ] as const satisfies readonly ToolFilter[]; interface ApiLogsResponse { @@ -120,7 +120,7 @@ type ViewMode = "chain" | "list"; function allowedToolsForTag(tag: LogTag, detailedLogs: boolean): readonly ToolFilter[] { if (detailedLogs) return ALLOWED_TOOLS[tag]; - if (tag === "memory_add" || tag === "memory_search") return ALLOWED_TOOLS[tag]; + if (tag === "memory_add" || tag === "memos_search") return ALLOWED_TOOLS[tag]; return BASIC_LOG_TOOLS; } @@ -154,7 +154,7 @@ export function LogsView() { useEffect(() => { if (detailedLogs) return; - if (tag !== "" && tag !== "memory_add" && tag !== "memory_search") { + if (tag !== "" && tag !== "memory_add" && tag !== "memos_search") { setTag(""); } if (failuresOnly) setFailuresOnly(false); @@ -547,7 +547,7 @@ function LogDetailBody({ input: unknown; output: unknown; }) { - if (log.toolName === "memory_search") { + if (log.toolName === "memos_search") { return ; } if (log.toolName === "memory_add") { @@ -573,7 +573,7 @@ function LogDetailBody({ return ; } -// ─── memory_search template ───────────────────────────────────────────── +// ─── memos_search template ───────────────────────────────────────────── interface SearchInput { query?: string; @@ -1311,7 +1311,7 @@ function parseJson(s: string): unknown { * the id. * * Precedence per tool: - * - memory_search → the query + kept/total counts + * - memos_search → the query + kept/total counts * - memory_add → first 3 per-turn summaries (already meaningful) * - skill_* → `output.name` (e.g. "write_python_function_with_types") * - policy_* → `output.title` (e.g. "Write Python function …") @@ -1323,7 +1323,7 @@ function buildSummary(log: ApiLogDTO, input: unknown, output: unknown): string { const inp = (input ?? {}) as Record; const out = (output ?? {}) as Record; - if (log.toolName === "memory_search") { + if (log.toolName === "memos_search") { const q = (inp.query as string | undefined) ?? "(empty)"; const kept = (out.filtered as unknown[] | undefined)?.length ?? 0; const totalN = (out.candidates as unknown[] | undefined)?.length ?? 0; @@ -1528,7 +1528,7 @@ function buildChainEvent(log: ApiLogDTO): ChainEvent { let stagePhase: string | undefined; let infraKind: ChainEvent["infraKind"]; - if (log.toolName === "memory_search") { + if (log.toolName === "memos_search") { stage = "retrieval"; sessionId = pickStr(inp.sessionId); episodeId = pickStr(inp.episodeId); diff --git a/apps/memos-local-plugin/viewer/src/views/OverviewView.tsx b/apps/memos-local-plugin/viewer/src/views/OverviewView.tsx index 0f6c31c9f..1c66a0c8c 100644 --- a/apps/memos-local-plugin/viewer/src/views/OverviewView.tsx +++ b/apps/memos-local-plugin/viewer/src/views/OverviewView.tsx @@ -303,7 +303,7 @@ function apiLogEventType( switch (log.toolName) { case "memory_add": return "trace.created"; - case "memory_search": + case "memos_search": return hasRetrievalHits(output) ? "retrieval.tier1.hit" : "retrieval.empty"; case "policy_generate": return "l2.induced"; diff --git a/apps/memos-local-plugin/website/index.html b/apps/memos-local-plugin/website/index.html index 4a2ea14e1..babe7da70 100644 --- a/apps/memos-local-plugin/website/index.html +++ b/apps/memos-local-plugin/website/index.html @@ -850,12 +850,12 @@

适配你的技术栈

宿主工具与Viewer 能力Host Tools and Viewer Capabilities

-
🔍

memory_search

三层检索Three-tier search

-
📄

memory_get

读取 trace / policy / world modelFetch trace / policy / world model

-
📜

memory_timeline

查看 episode 时间线Episode timeline

-
🌎

memory_environment

查询 L3 环境认知Query L3 world models

-

skill_list

列出候选和活跃技能List candidate and active skills

-
📘

skill_get

获取技能调用指南Fetch invocation guide

+
🔍

memos_search

三层检索Three-tier search

+
📄

memos_get

读取 trace / policy / world modelFetch trace / policy / world model

+
📜

memos_timeline

查看 episode 时间线Episode timeline

+
🌎

memos_environment

查询 L3 环境认知Query L3 world models

+

memos_skill_list

列出候选和活跃技能List candidate and active skills

+
📘

memos_skill_get

获取技能调用指南Fetch invocation guide

From 276d54717848ac517c3087b2dce4697b4dcfeedd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=91=E5=B8=83=E6=9E=97?= <11641432+heiheiyouyou@user.noreply.gitee.com> Date: Wed, 13 May 2026 17:01:35 +0800 Subject: [PATCH 02/38] fix:skill get --- .../core/pipeline/memory-core.ts | 26 ++++++++-- .../tests/unit/pipeline/memory-core.test.ts | 49 +++++++++++++++++++ 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/apps/memos-local-plugin/core/pipeline/memory-core.ts b/apps/memos-local-plugin/core/pipeline/memory-core.ts index ffaecbf1e..5c7a30c4b 100644 --- a/apps/memos-local-plugin/core/pipeline/memory-core.ts +++ b/apps/memos-local-plugin/core/pipeline/memory-core.ts @@ -2959,19 +2959,37 @@ export function createMemoryCore( ): Promise { ensureLive(); if (opts?.namespace) activeNamespace = opts.namespace; - const row = handle.repos.skills.getById(id); + const row = resolveSkillRowForGet(id, opts); if (!row || (!opts?.includeAllNamespaces && !visibleToCurrent(row))) return null; if (opts?.recordUse) { - handle.repos.skills.recordUse(id, Date.now()); + handle.repos.skills.recordUse(row.id, Date.now()); if (opts.recordTrial) { - recordSkillTrial(id, opts); + recordSkillTrial(row.id, opts); } - const updated = handle.repos.skills.getById(id); + const updated = handle.repos.skills.getById(row.id); return updated ? skillRowToDTO(updated) : skillRowToDTO(row); } return skillRowToDTO(row); } + function resolveSkillRowForGet( + id: SkillId, + opts?: { includeAllNamespaces?: boolean }, + ) { + const exact = handle.repos.skills.getById(id); + if (exact) return exact; + + const rawId = String(id); + const shortId = rawId.includes(":") ? rawId.slice(rawId.lastIndexOf(":") + 1) : rawId; + const candidates = handle.repos.skills.list({ limit: 5_000 }).filter((row) => { + if (!opts?.includeAllNamespaces && !visibleToCurrent(row)) return false; + if (row.name === rawId || row.name === shortId) return true; + if (rawId.includes(":")) return row.id === shortId; + return row.id.endsWith(`:${rawId}`); + }); + return candidates.length === 1 ? candidates[0]! : null; + } + function recordSkillTrial( skillId: SkillId, opts: { diff --git a/apps/memos-local-plugin/tests/unit/pipeline/memory-core.test.ts b/apps/memos-local-plugin/tests/unit/pipeline/memory-core.test.ts index 440b23976..af540de77 100644 --- a/apps/memos-local-plugin/tests/unit/pipeline/memory-core.test.ts +++ b/apps/memos-local-plugin/tests/unit/pipeline/memory-core.test.ts @@ -24,6 +24,7 @@ import { makeTmpDb, type TmpDbHandle } from "../../helpers/tmp-db.js"; import { makeTmpHome, type TmpHomeContext } from "../../helpers/tmp-home.js"; import { fakeEmbedder } from "../../helpers/fake-embedder.js"; import type { MemosError } from "../../../agent-contract/errors.js"; +import type { SkillId, SkillRow } from "../../../core/types.js"; let db: TmpDbHandle | null = null; let pipeline: PipelineHandle | null = null; @@ -55,6 +56,32 @@ function traceKind(trace: TraceDTO): string { : "assistant"); } +function seedCoreSkill(id: string, name: string): void { + const row: SkillRow = { + id: id as SkillId, + ownerAgentKind: "openclaw", + ownerProfileId: "main", + ownerWorkspaceId: null, + name, + status: "active", + invocationGuide: `${name}\n\nFollow the proven procedure.`, + procedureJson: null, + eta: 0.9, + support: 3, + gain: 0.3, + trialsAttempted: 0, + trialsPassed: 0, + sourcePolicyIds: [], + sourceWorldModelIds: [], + evidenceAnchors: [], + vec: null, + createdAt: 1_700_000_000_000 as SkillRow["createdAt"], + updatedAt: 1_700_000_000_000 as SkillRow["updatedAt"], + version: 1, + }; + db!.repos.skills.upsert(row); +} + beforeEach(() => { db = makeTmpDb(); }); @@ -833,6 +860,28 @@ describe("MemoryCore façade", () => { code: "already_shut_down", }); }); + + it("getSkill resolves colon-qualified skill ids and short aliases", async () => { + pipeline = createPipeline(buildDeps(db!)); + core = createMemoryCore( + pipeline, + resolveHome("openclaw", "/tmp/memos-mc-test"), + "test", + ); + await core.init(); + + seedCoreSkill("skillsbench:skill-a089bcb8e0258209", "skill-a089bcb8e0258209"); + seedCoreSkill("skill-local-only", "local skill"); + + await expect(core.getSkill("skill-a089bcb8e0258209" as SkillId)).resolves.toMatchObject({ + id: "skillsbench:skill-a089bcb8e0258209", + name: "skill-a089bcb8e0258209", + }); + await expect(core.getSkill("skillsbench:skill-local-only" as SkillId)).resolves.toMatchObject({ + id: "skill-local-only", + name: "local skill", + }); + }); }); describe("bootstrapMemoryCore", () => { From 9ed72fe2c5d41053f14bf3532b5c5a4b59e38e2a Mon Sep 17 00:00:00 2001 From: jiang Date: Wed, 13 May 2026 17:18:49 +0800 Subject: [PATCH 03/38] feat: add threshold for feedback --- .../core/config/defaults.ts | 1 + apps/memos-local-plugin/core/config/schema.ts | 8 ++ .../core/feedback/evidence.ts | 8 +- .../memos-local-plugin/core/feedback/types.ts | 8 ++ .../tests/unit/feedback/_helpers.ts | 1 + .../tests/unit/feedback/evidence.test.ts | 111 ++++++++++++++++++ 6 files changed, 134 insertions(+), 3 deletions(-) diff --git a/apps/memos-local-plugin/core/config/defaults.ts b/apps/memos-local-plugin/core/config/defaults.ts index 680957819..8c7355b1f 100644 --- a/apps/memos-local-plugin/core/config/defaults.ts +++ b/apps/memos-local-plugin/core/config/defaults.ts @@ -184,6 +184,7 @@ export const DEFAULT_CONFIG: ResolvedConfig = { failureThreshold: 3, failureWindow: 5, valueDelta: 0.5, + minLowValueThreshold: 0.01, useLlm: true, attachToPolicy: true, cooldownMs: 60_000, diff --git a/apps/memos-local-plugin/core/config/schema.ts b/apps/memos-local-plugin/core/config/schema.ts index 675cade70..ea3072841 100644 --- a/apps/memos-local-plugin/core/config/schema.ts +++ b/apps/memos-local-plugin/core/config/schema.ts @@ -253,6 +253,14 @@ const AlgorithmSchema = Type.Object({ failureWindow: NumberInRange(5, 2, 50), /** Min |mean(high) - mean(low)| to fire without an explicit user signal. */ valueDelta: NumberInRange(0.5, 0, 2), + /** + * Minimum absolute value threshold for lowValue traces. Only traces with + * value < -minLowValueThreshold will be collected as failure evidence + * (unless they match isFailureLike patterns). This filters out trivial + * negative feedback (e.g., value = -0.001) and focuses on genuine failures. + * Default 0.01 — adjust higher (e.g., 0.1) to be more conservative. + */ + minLowValueThreshold: NumberInRange(0.01, 0, 1), /** Let the LLM rewrite the preference / anti-pattern lines. */ useLlm: Bool(true), /** Tag the L2 policies referenced by the evidence with the guidance. */ diff --git a/apps/memos-local-plugin/core/feedback/evidence.ts b/apps/memos-local-plugin/core/feedback/evidence.ts index ba7ec9217..4bcaa0368 100644 --- a/apps/memos-local-plugin/core/feedback/evidence.ts +++ b/apps/memos-local-plugin/core/feedback/evidence.ts @@ -44,6 +44,7 @@ export function gatherRepairEvidence( ): EvidenceResult { const cap = input.limit ?? deps.config.evidenceLimit; const needle = input.keyword?.toLowerCase().trim() ?? ""; + const minLowValueThreshold = deps.config.minLowValueThreshold; // Pull a generous recent batch and split by value sign. Limiting at the // SQL layer is fine because the caller passes a small `limit`. @@ -64,7 +65,7 @@ export function gatherRepairEvidence( // empty we keep the filtered result — the synthesizer is designed to // fall back to template output in that case without needing extra // context. - const firstPass = partition(recent, cap, needle); + const firstPass = partition(recent, cap, needle, minLowValueThreshold); const firstPassEmpty = firstPass.highValue.length === 0 && firstPass.lowValue.length === 0; if (!needle || !firstPassEmpty) { @@ -77,7 +78,7 @@ export function gatherRepairEvidence( }); return firstPass; } - const relaxed = partition(recent, cap, ""); + const relaxed = partition(recent, cap, "", minLowValueThreshold); deps.log.debug("evidence.gathered", { sessionId: input.sessionId, highValue: relaxed.highValue.length, @@ -92,6 +93,7 @@ function partition( traces: readonly TraceRow[], cap: number, needle: string, + minLowValueThreshold: number, ): EvidenceResult { const highValue: TraceRow[] = []; const lowValue: TraceRow[] = []; @@ -99,7 +101,7 @@ function partition( if (needle && !traceContains(trace, needle)) continue; if (trace.value > 0) { if (highValue.length < cap) highValue.push(trace); - } else if (trace.value < 0 || isFailureLike(trace)) { + } else if (trace.value < -minLowValueThreshold || isFailureLike(trace)) { if (lowValue.length < cap) lowValue.push(trace); } if (highValue.length >= cap && lowValue.length >= cap) break; diff --git a/apps/memos-local-plugin/core/feedback/types.ts b/apps/memos-local-plugin/core/feedback/types.ts index 3df34604b..0373bc31d 100644 --- a/apps/memos-local-plugin/core/feedback/types.ts +++ b/apps/memos-local-plugin/core/feedback/types.ts @@ -44,6 +44,14 @@ export interface FeedbackConfig { * value-guided comparison to fire. V7 §2.4.6 → `δ ≈ 0.5`. */ valueDelta: number; + /** + * Minimum absolute value threshold for lowValue traces. Only traces with + * value < -minLowValueThreshold will be collected as failure evidence + * (unless they match isFailureLike patterns). This filters out trivial + * negative feedback (e.g., value = -0.001) and focuses on genuine failures. + * Default 0.01 — adjust higher (e.g., 0.1) to be more conservative. + */ + minLowValueThreshold: number; /** * Call the LLM to produce the final preference / anti-pattern lines. * When false, fall back to a simple template using the most relevant diff --git a/apps/memos-local-plugin/tests/unit/feedback/_helpers.ts b/apps/memos-local-plugin/tests/unit/feedback/_helpers.ts index c318c10b4..252ebd376 100644 --- a/apps/memos-local-plugin/tests/unit/feedback/_helpers.ts +++ b/apps/memos-local-plugin/tests/unit/feedback/_helpers.ts @@ -34,6 +34,7 @@ export function makeFeedbackConfig( failureThreshold: 3, failureWindow: 5, valueDelta: 0.5, + minLowValueThreshold: 0.01, useLlm: true, attachToPolicy: true, cooldownMs: 60_000, diff --git a/apps/memos-local-plugin/tests/unit/feedback/evidence.test.ts b/apps/memos-local-plugin/tests/unit/feedback/evidence.test.ts index e0d5a8b1b..6fcc82505 100644 --- a/apps/memos-local-plugin/tests/unit/feedback/evidence.test.ts +++ b/apps/memos-local-plugin/tests/unit/feedback/evidence.test.ts @@ -220,4 +220,115 @@ describe("feedback/evidence", () => { expect(capped.userText.startsWith("...")).toBe(true); expect(capTrace(trace, 0)).toBe(trace); // no-op }); + + it("filters out trivial negative values below minLowValueThreshold", () => { + handle = makeTmpDb(); + const h = handle; + const sessionId = "s6"; + const episodeId = "ep6" as EpisodeId; + + // Trivial negative values (should be filtered out) + seedTrace(h, { + episodeId: episodeId as string, + sessionId, + agentText: "slightly not perfect", + value: -0.001, + }); + seedTrace(h, { + episodeId: episodeId as string, + sessionId, + agentText: "almost neutral", + value: -0.005, + }); + + // Genuine failure (should be collected) + seedTrace(h, { + episodeId: episodeId as string, + sessionId, + agentText: "real failure", + value: -0.2, + }); + + const res = gatherRepairEvidence( + { sessionId: sessionId as SessionId }, + { + repos: h.repos, + config: makeFeedbackConfig({ minLowValueThreshold: 0.01 }), + log: rootLogger.child({ channel: "test.evidence" }), + }, + ); + + // Only the genuine failure should be collected + expect(res.lowValue).toHaveLength(1); + expect(res.lowValue[0]!.value).toBe(-0.2); + }); + + it("collects traces with error keywords even if value is above threshold", () => { + handle = makeTmpDb(); + const h = handle; + const sessionId = "s7"; + const episodeId = "ep7" as EpisodeId; + + // Small negative value but has error keyword (should be collected) + seedTrace(h, { + episodeId: episodeId as string, + sessionId, + agentText: "error: connection timeout", + value: -0.005, + }); + + // Small negative value without error keyword (should be filtered) + seedTrace(h, { + episodeId: episodeId as string, + sessionId, + agentText: "task completed but user slightly unhappy", + value: -0.005, + }); + + const res = gatherRepairEvidence( + { sessionId: sessionId as SessionId }, + { + repos: h.repos, + config: makeFeedbackConfig({ minLowValueThreshold: 0.01 }), + log: rootLogger.child({ channel: "test.evidence" }), + }, + ); + + // Only the one with error keyword should be collected + expect(res.lowValue).toHaveLength(1); + expect(res.lowValue[0]!.agentText).toContain("error"); + }); + + it("respects custom minLowValueThreshold config", () => { + handle = makeTmpDb(); + const h = handle; + const sessionId = "s8"; + const episodeId = "ep8" as EpisodeId; + + seedTrace(h, { + episodeId: episodeId as string, + sessionId, + agentText: "minor issue", + value: -0.05, + }); + seedTrace(h, { + episodeId: episodeId as string, + sessionId, + agentText: "moderate failure", + value: -0.15, + }); + + // With threshold 0.1, only -0.15 should be collected + const res = gatherRepairEvidence( + { sessionId: sessionId as SessionId }, + { + repos: h.repos, + config: makeFeedbackConfig({ minLowValueThreshold: 0.1 }), + log: rootLogger.child({ channel: "test.evidence" }), + }, + ); + + expect(res.lowValue).toHaveLength(1); + expect(res.lowValue[0]!.value).toBe(-0.15); + }); }); From 97dc73e73301b80d7c6669282a55c66bc3edec8f Mon Sep 17 00:00:00 2001 From: jiang Date: Wed, 13 May 2026 19:46:10 +0800 Subject: [PATCH 04/38] fix: memory leak issue --- apps/memos-local-plugin/.gitignore | 1 + .../hermes/memos_provider/__init__.py | 49 +++++++++++++------ .../hermes/memos_provider/bridge_client.py | 37 +++++++++++--- .../hermes/memos_provider/daemon_manager.py | 45 +++++++++++++++++ 4 files changed, 111 insertions(+), 21 deletions(-) diff --git a/apps/memos-local-plugin/.gitignore b/apps/memos-local-plugin/.gitignore index 0c52ba684..1df7952e4 100644 --- a/apps/memos-local-plugin/.gitignore +++ b/apps/memos-local-plugin/.gitignore @@ -17,6 +17,7 @@ coverage/ TODO.local.md AGENTS_*.md .test_* +.claude # ARMS telemetry credentials — generated by CI from secrets before # `npm publish` (see scripts/generate-telemetry-credentials.cjs and diff --git a/apps/memos-local-plugin/adapters/hermes/memos_provider/__init__.py b/apps/memos-local-plugin/adapters/hermes/memos_provider/__init__.py index 4ea213f35..e47c53e7b 100644 --- a/apps/memos-local-plugin/adapters/hermes/memos_provider/__init__.py +++ b/apps/memos-local-plugin/adapters/hermes/memos_provider/__init__.py @@ -239,6 +239,7 @@ class MemTensorProvider(MemoryProvider): def __init__(self) -> None: self._bridge: MemosBridgeClient | None = None + self._reconnect_lock = threading.Lock() self._session_id: str = "" self._episode_id: str = "" self._hermes_home: str = "" @@ -1426,16 +1427,16 @@ def on_session_end(self, messages: list[dict[str, Any]]) -> None: # type: ignor def shutdown(self) -> None: # type: ignore[override] self._bridge_keepalive_stop.set() if self._bridge_keepalive_thread and self._bridge_keepalive_thread.is_alive(): - self._bridge_keepalive_thread.join(timeout=2.0) + self._bridge_keepalive_thread.join(timeout=12.0) # Increased to cover health check timeout (10s) + margin if self._prefetch_thread and self._prefetch_thread.is_alive(): self._prefetch_thread.join(timeout=5.0) if self._bridge: + pid = self._bridge.pid + logger.info("MemOS: shutting down bridge (pid=%s)", pid) with contextlib.suppress(Exception): self._bridge.close() self._bridge = None - # DON'T call shutdown_bridge() — the bridge process stays alive - # as a daemon if its viewer is running, so the memory panel - # remains accessible between `hermes chat` sessions. + logger.info("MemOS: bridge shutdown complete (pid=%s)", pid) # ─── Host LLM bridge (fallback for plugin-side model failures) ──────── @@ -1637,17 +1638,35 @@ def _is_transport_closed(self, err: Exception) -> bool: return "broken pipe" in msg or "bridge closed" in msg or "transport_closed" in msg def _reconnect_bridge(self, session_id: str = "", *, timeout: float = 30.0) -> None: - old_bridge = self._bridge - if old_bridge: - with contextlib.suppress(Exception): - old_bridge.close() - ensure_bridge_running() - self._bridge = MemosBridgeClient() - self._bridge.register_host_handler( - "host.llm.complete", - self._handle_host_llm_complete, - ) - self._open_session(session_id, timeout=timeout) + # Don't reconnect if we're shutting down + if self._bridge_keepalive_stop.is_set(): + logger.debug("MemOS: skipping reconnect during shutdown") + return + + with self._reconnect_lock: + # Double-check after acquiring lock + if self._bridge_keepalive_stop.is_set(): + logger.debug("MemOS: skipping reconnect during shutdown (after lock)") + return + + old_bridge = self._bridge + old_pid = old_bridge.pid if old_bridge else None + + if old_bridge: + logger.info("MemOS: closing old bridge (pid=%s)", old_pid) + with contextlib.suppress(Exception): + old_bridge.close() + logger.info("MemOS: old bridge closed (pid=%s)", old_pid) + + ensure_bridge_running() + self._bridge = MemosBridgeClient() + logger.info("MemOS: new bridge created (pid=%s)", self._bridge.pid) + + self._bridge.register_host_handler( + "host.llm.complete", + self._handle_host_llm_complete, + ) + self._open_session(session_id, timeout=timeout) def _ensure_bridge(self, session_id: str = "", *, timeout: float = 30.0) -> bool: if self._bridge: diff --git a/apps/memos-local-plugin/adapters/hermes/memos_provider/bridge_client.py b/apps/memos-local-plugin/adapters/hermes/memos_provider/bridge_client.py index 25396ccb9..e2808633b 100644 --- a/apps/memos-local-plugin/adapters/hermes/memos_provider/bridge_client.py +++ b/apps/memos-local-plugin/adapters/hermes/memos_provider/bridge_client.py @@ -144,6 +144,11 @@ def __init__( ) self._stderr_reader.start() + @property + def pid(self) -> int: + """Return the PID of the bridge subprocess.""" + return self._proc.pid + # ─── Public API ── def request( @@ -222,14 +227,34 @@ def close(self) -> None: if self._closed: return self._closed = True + + pid = self._proc.pid + + # 1. Close stdin (triggers bridge's graceful exit) with contextlib.suppress(Exception): self._proc.stdin.close() - # DON'T wait() or kill() the bridge process. If it has an - # active viewer (HTTP server), it will stay alive as a daemon - # so the memory panel remains accessible between `hermes chat` - # sessions. If it's headless (viewer port was taken), it will - # notice stdin EOF and exit on its own. - # unblock any pending waiters + + # 2. Wait for process to exit gracefully (up to 5 seconds) + try: + self._proc.wait(timeout=5.0) + logger.debug("MemOS: bridge process %d exited gracefully", pid) + except subprocess.TimeoutExpired: + # 3. If still running, send SIGTERM + logger.warning("MemOS: bridge process %d did not exit after stdin close, sending SIGTERM", pid) + try: + self._proc.terminate() # Send SIGTERM + self._proc.wait(timeout=5.0) # Increased from 2.0 to 5.0 for viewer cleanup + logger.debug("MemOS: bridge process %d terminated", pid) + except subprocess.TimeoutExpired: + # 4. Last resort: SIGKILL + logger.error("MemOS: bridge process %d did not respond to SIGTERM, sending SIGKILL", pid) + self._proc.kill() # Send SIGKILL + try: + self._proc.wait(timeout=1.0) + except subprocess.TimeoutExpired: + logger.error("MemOS: bridge process %d could not be killed", pid) + + # 5. Clean up pending requests with self._lock: for entry in list(self._pending.values()): entry["error"] = { diff --git a/apps/memos-local-plugin/adapters/hermes/memos_provider/daemon_manager.py b/apps/memos-local-plugin/adapters/hermes/memos_provider/daemon_manager.py index 62810cc5b..bc3ae6a3b 100644 --- a/apps/memos-local-plugin/adapters/hermes/memos_provider/daemon_manager.py +++ b/apps/memos-local-plugin/adapters/hermes/memos_provider/daemon_manager.py @@ -17,9 +17,12 @@ from __future__ import annotations import logging +import os +import signal import shutil import subprocess import threading +import time from pathlib import Path @@ -74,3 +77,45 @@ def shutdown_bridge() -> None: global _bridge_ok with _lock: _bridge_ok = None + + +def wait_for_process_exit(pid: int, timeout: float = 5.0) -> bool: + """Wait for a process to exit. + + Returns True if the process has exited, False if still running after timeout. + """ + start = time.time() + while time.time() - start < timeout: + try: + # Check if process exists (signal 0 doesn't actually send a signal) + os.kill(pid, 0) + time.sleep(0.1) + except (OSError, ProcessLookupError): + # Process doesn't exist = has exited + return True + return False + + +def terminate_bridge_process(pid: int, timeout: float = 7.0) -> bool: + """Terminate a bridge process gracefully, then forcefully if needed. + + Returns True if the process was successfully terminated. + """ + try: + # Check if process exists first + os.kill(pid, 0) + except (OSError, ProcessLookupError): + return True # Already gone + + try: + # 1. Send SIGTERM (graceful shutdown) + os.kill(pid, signal.SIGTERM) + if wait_for_process_exit(pid, timeout=5.0): + return True + + # 2. If still running, send SIGKILL (force kill) + logger.warning("MemOS: bridge process %d did not exit after SIGTERM, sending SIGKILL", pid) + os.kill(pid, signal.SIGKILL) + return wait_for_process_exit(pid, timeout=2.0) + except (OSError, ProcessLookupError): + return True From 86fe120452901ddda990580a46a9b8982c013be9 Mon Sep 17 00:00:00 2001 From: jiang Date: Thu, 14 May 2026 15:05:28 +0800 Subject: [PATCH 05/38] feat: configure embedding rebuild batch size --- apps/memos-local-plugin/package-lock.json | 4 +- apps/memos-local-plugin/package.json | 2 +- .../viewer/src/stores/i18n.ts | 7 +++ .../viewer/src/views/SettingsView.tsx | 45 ++++++++++++++++++- 4 files changed, 54 insertions(+), 4 deletions(-) diff --git a/apps/memos-local-plugin/package-lock.json b/apps/memos-local-plugin/package-lock.json index 97304d334..d7400fffd 100644 --- a/apps/memos-local-plugin/package-lock.json +++ b/apps/memos-local-plugin/package-lock.json @@ -1,12 +1,12 @@ { "name": "@memtensor/memos-local-plugin", - "version": "2.0.1", + "version": "2.0.2-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@memtensor/memos-local-plugin", - "version": "2.0.1", + "version": "2.0.2-beta.1", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/apps/memos-local-plugin/package.json b/apps/memos-local-plugin/package.json index 4ff8d66bb..6fde62877 100644 --- a/apps/memos-local-plugin/package.json +++ b/apps/memos-local-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@memtensor/memos-local-plugin", - "version": "2.0.1", + "version": "2.0.2-beta.1", "description": "Reflect2Evolve memory plugin: layered L1/L2/L3 memory, reflection-weighted value backprop, cross-task policy induction, skill crystallization, three-tier retrieval. Adapters for OpenClaw and Hermes Agent via a shared algorithm core.", "type": "module", "main": "dist/core/index.js", diff --git a/apps/memos-local-plugin/viewer/src/stores/i18n.ts b/apps/memos-local-plugin/viewer/src/stores/i18n.ts index f4da01399..4ce9e684f 100644 --- a/apps/memos-local-plugin/viewer/src/stores/i18n.ts +++ b/apps/memos-local-plugin/viewer/src/stores/i18n.ts @@ -825,6 +825,10 @@ const en = { "Ready {ready}/{total}; missing {missing}; dimension mismatch {mismatch}; current dim {dim}.", "settings.embedding.maintenance.unavailable": "Configure an embedding provider before repairing or rebuilding vectors.", + "settings.embedding.batchSize.label": "Items per request", + "settings.embedding.batchSize.option": "{n} items per request", + "settings.embedding.batchSize.hint": + "Larger batches usually rebuild faster, but may hit provider limits or timeouts.", "settings.embedding.repair": "Repair missing/mismatched", "settings.embedding.rebuild": "Rebuild all vectors", "settings.embedding.rebuild.running": "Rebuilding embeddings…", @@ -1621,6 +1625,9 @@ const zh: Record = { "settings.embedding.maintenance.stats": "可用 {ready}/{total};缺失 {missing};维度不匹配 {mismatch};当前维度 {dim}。", "settings.embedding.maintenance.unavailable": "请先配置嵌入模型,再修复或重建向量。", + "settings.embedding.batchSize.label": "每次请求条数", + "settings.embedding.batchSize.option": "每次请求 {n} 条", + "settings.embedding.batchSize.hint": "每次请求条数越大通常重建越快,但可能触发模型服务限流或超时。", "settings.embedding.repair": "修复缺失/错维", "settings.embedding.rebuild": "全量重建向量", "settings.embedding.rebuild.running": "正在重建向量…", diff --git a/apps/memos-local-plugin/viewer/src/views/SettingsView.tsx b/apps/memos-local-plugin/viewer/src/views/SettingsView.tsx index 20993af62..98b2f3371 100644 --- a/apps/memos-local-plugin/viewer/src/views/SettingsView.tsx +++ b/apps/memos-local-plugin/viewer/src/views/SettingsView.tsx @@ -71,6 +71,10 @@ interface EmbeddingMaintenanceRunResult { error?: string; } +const EMBEDDING_REBUILD_BATCH_STORAGE_KEY = "memos.embeddingRebuildBatchSize"; +const EMBEDDING_REBUILD_BATCH_OPTIONS = [10, 20, 50, 100, 200, 500] as const; +type EmbeddingRebuildBatchSize = typeof EMBEDDING_REBUILD_BATCH_OPTIONS[number]; + const EMBEDDING_PROVIDERS = [ "local", "openai_compatible", @@ -536,6 +540,7 @@ function EmbeddingMaintenancePanel() { const [stats, setStats] = useState(null); const [running, setRunning] = useState<"repair" | "rebuild" | null>(null); const [status, setStatus] = useState<{ kind: "ok" | "error" | "muted"; text: string } | null>(null); + const [batchSize, setBatchSize] = useState(() => loadEmbeddingRebuildBatchSize()); const refresh = async () => { try { @@ -559,7 +564,7 @@ function EmbeddingMaintenancePanel() { for (;;) { const r = await api.post( "/api/v1/embeddings/rebuild", - { mode, offset, limit: 100 }, + { mode, offset, limit: batchSize }, ); updated += r.updated; failed += r.failed; @@ -616,6 +621,33 @@ function EmbeddingMaintenancePanel() {
{healthText}
+ +
+ {t("settings.embedding.batchSize.hint")} +
-
- {/* - * Refresh — mirrors MemoriesView. Clears search + status - * filter, drops selection, and re-fetches page 0 so the user - * sees freshly-induced policies without a full page reload. - */} - -
- - - {/* Row 1: search box */} -
- -
- - {/* Row 2: filter chips — own row, matches TasksView / MemoriesView */} -
-
- {statuses.map((s) => ( + {!lightweight.enabled && ( +
+ {/* + * Refresh — mirrors MemoriesView. Clears search + status + * filter, drops selection, and re-fetches page 0 so the user + * sees freshly-induced policies without a full page reload. + */} - ))} -
- +
+ )}
- {loading && rows.length === 0 && ( + {lightweight.loading && (
{[0, 1, 2].map((i) => (
))}
)} - {!loading && rows.length === 0 && ( -
-
-
{t("policies.empty")}
-
{t("policies.empty.hint")}
-
+ + {!lightweight.loading && lightweight.enabled && ( + )} - {rows.length > 0 && ( -
- {rows.map((p) => { - const isSel = selected.has(p.id); - return ( -
setDetail(p)} - > - -
-
{p.title || "(untitled)"}
-
- - {t(`status.${p.status}` as never)} - support {p.support} - gain {p.gain.toFixed(2)} - {(p.preference?.length ?? 0) > 0 && ( - - {t("policies.guidance.prefer")} {p.preference.length} - - )} - {(p.antiPattern?.length ?? 0) > 0 && ( - - {t("policies.guidance.avoid")} {p.antiPattern.length} - - )} - {new Date(p.updatedAt).toLocaleString()} + {!lightweight.loading && !lightweight.enabled && ( + <> + {/* Row 1: search box */} +
+ +
+ + {/* Row 2: filter chips — own row, matches TasksView / MemoriesView */} +
+
+ {statuses.map((s) => ( + + ))} +
+ +
+ + {loading && rows.length === 0 && ( +
+ {[0, 1, 2].map((i) => ( +
+ ))} +
+ )} + {!loading && rows.length === 0 && ( +
+
+
{t("policies.empty")}
+
{t("policies.empty.hint")}
+
+ )} + + {rows.length > 0 && ( +
+ {rows.map((p) => { + const isSel = selected.has(p.id); + return ( +
setDetail(p)} + > + +
+
{p.title || "(untitled)"}
+
+ + {t(`status.${p.status}` as never)} + support {p.support} + gain {p.gain.toFixed(2)} + {(p.preference?.length ?? 0) > 0 && ( + + {t("policies.guidance.prefer")} {p.preference.length} + + )} + {(p.antiPattern?.length ?? 0) > 0 && ( + + {t("policies.guidance.avoid")} {p.antiPattern.length} + + )} + {new Date(p.updatedAt).toLocaleString()} +
+
+ {/* + * Lifecycle actions live in the drawer footer (PolicyDrawer). + * The row itself stays clean with just title + meta + chevron, + * matching the other list views. + */} +
+ +
-
- {/* - * Lifecycle actions live in the drawer footer (PolicyDrawer). - * The row itself stays clean with just title + meta + chevron, - * matching the other list views. - */} -
- -
+ ); + })}
- ); - })} -
- )} + )} - {(page > 0 || hasMore) && ( - { - void load({ q: query.trim(), status, page: nextPage }); - }} - /> - )} + {(page > 0 || hasMore) && ( + { + void load({ q: query.trim(), status, page: nextPage }); + }} + /> + )} - {detail && ( - { - setDetail(null); - clearEntryId(); - }} - onUpdated={(updated) => { - setRows((prev) => prev.map((r) => (r.id === updated.id ? updated : r))); - setDetail(updated); - }} - onStatusChange={async (p, next) => { - await setPolicyStatus(p, next); - // refresh the drawer with the new status. - setDetail((cur) => (cur ? { ...cur, status: next } : cur)); - }} - onDelete={(p) => deletePolicy(p)} - /> - )} + {detail && ( + { + setDetail(null); + clearEntryId(); + }} + onUpdated={(updated) => { + setRows((prev) => prev.map((r) => (r.id === updated.id ? updated : r))); + setDetail(updated); + }} + onStatusChange={async (p, next) => { + await setPolicyStatus(p, next); + // refresh the drawer with the new status. + setDetail((cur) => (cur ? { ...cur, status: next } : cur)); + }} + onDelete={(p) => deletePolicy(p)} + /> + )} - {selected.size > 0 && ( -
- - {t("common.selected", { n: selected.size })} - - - -
- -
+ {selected.size > 0 && ( +
+ + {t("common.selected", { n: selected.size })} + + + +
+ +
+ )} + )} {toast && ( diff --git a/apps/memos-local-plugin/viewer/src/views/SettingsView.tsx b/apps/memos-local-plugin/viewer/src/views/SettingsView.tsx index 98b2f3371..eb71912b8 100644 --- a/apps/memos-local-plugin/viewer/src/views/SettingsView.tsx +++ b/apps/memos-local-plugin/viewer/src/views/SettingsView.tsx @@ -1,12 +1,12 @@ /** - * Settings view — four tabs: + * Settings view — three tabs: * * - AI Models — embedding / summarizer / **skill evolver** slots, * each with a "测试" button that calls * `POST /api/v1/models/test`. * - Team Sharing — hub on/off + address + tokens. - * - Account — optional password protection for the viewer. - * - General — theme + language + telemetry. + * - General — language, theme, lightweight memory, logging, + * telemetry, password protection, and danger zone. * * Save flow: `PATCH /api/v1/config` → show restart overlay → call * `POST /api/v1/admin/restart` → poll `GET /api/v1/health` until the @@ -30,13 +30,19 @@ interface ProviderBlock { temperature?: number; } +interface AlgorithmBlock { + lightweightMemory?: { + enabled?: boolean; + }; +} + interface ResolvedConfig { version?: number; viewer?: { port: number; bindHost?: string }; embedding?: ProviderBlock; llm?: ProviderBlock; skillEvolver?: ProviderBlock; - algorithm?: unknown; + algorithm?: AlgorithmBlock; hub?: { enabled?: boolean; role?: "hub" | "client"; @@ -234,8 +240,10 @@ export function SettingsView({ initialTab }: { initialTab?: Tab } = {}) { } logging={(get("logging") ?? {}) as NonNullable} + algorithm={(get("algorithm") ?? {}) as AlgorithmBlock} onPatchTelemetry={(p) => patch("telemetry", p)} onPatchLogging={(p) => patch("logging", p)} + onPatchAlgorithm={(p) => patch("algorithm", p)} /> )} @@ -820,24 +828,26 @@ function HubTab({ ); } -// ─── Account / password tab ────────────────────────────────────────────── - // ─── General tab (merged Account + General) ───────────────────────── function GeneralTab({ telemetry, logging, + algorithm, onPatchTelemetry, onPatchLogging, + onPatchAlgorithm, }: { telemetry: NonNullable; logging: NonNullable; + algorithm: AlgorithmBlock; onPatchTelemetry: ( p: Partial>, ) => void; onPatchLogging: ( p: Partial>, ) => void; + onPatchAlgorithm: (p: Partial) => void; }) { return (
@@ -886,7 +896,18 @@ function GeneralTab({
- +
+
+
+

{t("settings.general.lightweightMemory")}

+

{t("settings.general.lightweightMemory.desc")}

+
+ onPatchAlgorithm({ lightweightMemory: { enabled: v } })} + /> +
+
@@ -914,6 +935,8 @@ function GeneralTab({
+ +
); diff --git a/apps/memos-local-plugin/viewer/src/views/SkillsView.tsx b/apps/memos-local-plugin/viewer/src/views/SkillsView.tsx index 312aa4cc3..9ea73f3c5 100644 --- a/apps/memos-local-plugin/viewer/src/views/SkillsView.tsx +++ b/apps/memos-local-plugin/viewer/src/views/SkillsView.tsx @@ -16,6 +16,7 @@ import { t } from "../stores/i18n"; import { Icon } from "../components/Icon"; import { Pager } from "../components/Pager"; import { ShareScopePill } from "../components/ShareScopePill"; +import { LightweightModeEmpty } from "../components/LightweightModeEmpty"; import { NamespaceSelect, appendNamespaceParams } from "../components/NamespaceSelect"; import { Markdown } from "../components/Markdown"; import { route } from "../stores/router"; @@ -23,6 +24,7 @@ import { clearEntryId, linkTo } from "../stores/cross-link"; import type { CoreEvent, SkillDTO } from "../api/types"; import { areAllIdsSelected, toggleIdsInSelection } from "../utils/selection"; import { loadHubSharingEnabled } from "../utils/share"; +import { useLightweightMemoryMode } from "../hooks/useLightweightMemoryMode"; interface SkillUsage { sourcePolicies: Array<{ @@ -70,6 +72,7 @@ export function SkillsView() { const [selected, setSelected] = useState>(new Set()); const [refusalNotices, setRefusalNotices] = useState([]); const [showRefusalNotices, setShowRefusalNotices] = useState(false); + const lightweight = useLightweightMemoryMode(); const toggleSel = (id: string) => { setSelected((prev) => { const n = new Set(prev); @@ -109,8 +112,19 @@ export function SkillsView() { } }; useEffect(() => { + if (lightweight.loading || lightweight.enabled) return; void load(0); - }, [status, pageSize, namespaceFilter]); + }, [status, pageSize, namespaceFilter, lightweight.loading, lightweight.enabled]); + + useEffect(() => { + if (!lightweight.enabled) return; + setSkills([]); + setDetail(null); + setSelected(new Set()); + setHasMore(false); + setTotal(0); + setPage(0); + }, [lightweight.enabled]); useEffect(() => { const handle = openSse("/api/v1/events", (_, data) => { @@ -138,6 +152,7 @@ export function SkillsView() { // Deep-link: `#/skills?id=sk_xxx` auto-opens the drawer. useEffect(() => { + if (lightweight.loading || lightweight.enabled) return; const id = route.value.params.id; if (!id) return; const ctrl = new AbortController(); @@ -152,7 +167,7 @@ export function SkillsView() { }) .catch(() => void 0); return () => ctrl.abort(); - }, [route.value.params.id]); + }, [route.value.params.id, lightweight.loading, lightweight.enabled]); const filtered = (skills ?? []).filter((s) => { if (!query) return true; @@ -172,76 +187,44 @@ export function SkillsView() {

{t("skills.title")}

{t("skills.subtitle")}

-
- setShowRefusalNotices((v) => !v)} - onClear={() => { - setRefusalNotices([]); - setShowRefusalNotices(false); - }} - /> - {/* - * Refresh — matches MemoriesView / TasksView / PoliciesView / - * WorldModelsView. Clears search + status filter, drops - * selection, and re-fetches page 0 so the list visibly - * snaps back to "fresh top state". The old implementation - * only re-queried the CURRENT page with the CURRENT filters - * still applied, which looked like a no-op whenever the - * filtered slice hadn't actually changed. - */} - -
-
- -
- -
- -
-
- {[ - { v: "" as StatusFilter, k: "common.all" as const }, - { v: "active" as StatusFilter, k: "status.active" as const }, - { v: "candidate" as StatusFilter, k: "status.candidate" as const }, - { v: "archived" as StatusFilter, k: "status.archived" as const }, - ].map((opt) => ( + {!lightweight.enabled && ( +
+ setShowRefusalNotices((v) => !v)} + onClear={() => { + setRefusalNotices([]); + setShowRefusalNotices(false); + }} + /> + {/* + * Refresh — matches MemoriesView / TasksView / PoliciesView / + * WorldModelsView. Clears search + status filter, drops + * selection, and re-fetches page 0 so the list visibly + * snaps back to "fresh top state". The old implementation + * only re-queried the CURRENT page with the CURRENT filters + * still applied, which looked like a no-op whenever the + * filtered slice hadn't actually changed. + */} - ))} -
- +
+ )}
- {loading && ( + {lightweight.loading && (
{[0, 1, 2].map((i) => (
@@ -249,148 +232,201 @@ export function SkillsView() {
)} - {!loading && filtered.length === 0 && ( -
-
- -
-
{t("skills.empty")}
-
{t("skills.empty.hint")}
-
+ {!lightweight.loading && lightweight.enabled && ( + )} - {filtered.length > 0 && ( -
- {filtered.map((s) => { - const isSel = selected.has(s.id); - return ( -
setDetail(s)} - > -