diff --git a/docs/integrations/pi-agent.mdx b/docs/integrations/pi-agent.mdx index 82e7a2d0b..6114f9cc1 100644 --- a/docs/integrations/pi-agent.mdx +++ b/docs/integrations/pi-agent.mdx @@ -59,6 +59,7 @@ For advanced settings, create `~/.pi/agent/mem0-config.json`: "userId": "your-username", "autoCapture": true, "defaultScope": "project", + "searchThreshold": 0.3, "dream": { "enabled": true, "auto": true, @@ -75,12 +76,14 @@ For advanced settings, create `~/.pi/agent/mem0-config.json`: | `userId` | `string` | `$MEM0_USER_ID` or `"default"` | User identity for memory scoping | | `autoCapture` | `boolean` | `true` | Store facts from conversations automatically | | `defaultScope` | `string` | `"project"` | Default memory scope: `project`, `session`, or `global` | +| `searchThreshold` | `number` | `0.3` | Minimum similarity score (0–1) a memory must reach to count as a match for `/mem0-search`, `/mem0-forget`, and `/mem0-pin`, enforced on each result's relevance score. Raise it to be stricter; lower it if relevant results are missed. | | `dream.enabled` | `boolean` | `true` | Enable dream consolidation | | `dream.auto` | `boolean` | `true` | Auto-trigger dreams when thresholds are met | | `dream.minHours` | `number` | `24` | Minimum hours between auto-dreams | | `dream.minSessions` | `number` | `5` | Minimum sessions before first auto-dream | | `dream.minMemories` | `number` | `20` | Minimum memories before auto-dream triggers | + ## What's Included | Component | Description | diff --git a/integrations/pi-agent-plugin/CHANGELOG.md b/integrations/pi-agent-plugin/CHANGELOG.md index aef304a95..7a9656663 100644 --- a/integrations/pi-agent-plugin/CHANGELOG.md +++ b/integrations/pi-agent-plugin/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## 0.1.2 (2026-06-12) + +### Fixed + +- **Visible command feedback** — `/mem0-remember`, `/mem0-forget`, `/mem0-pin`, and `/mem0-scope` now render their results as persistent message blocks (`pi.sendMessage({ display: true })`) instead of `ctx.ui.notify(..., "info")`. The Pi TUI draws `"info"` notifications as dim, collapsible status text that overwrites the previous line, so command results were easily missed (felt like "no feedback"). Warnings and errors still use `ctx.ui.notify`, which renders prominently. +- **Relevance-filtered search** — `/mem0-search`, `/mem0-forget`, and `/mem0-pin` pass a similarity `threshold` (`searchThreshold`, default `0.3`; configurable in `mem0-config.json`, shown in `/mem0-status`), `top_k`, and `rerank` to the mem0 search API, matching the Claude Code and OpenClaw integrations. mem0 ranks results by similarity with no relevance floor, so without a threshold an unrelated query returns the closest (weak) memories — and `/mem0-forget` would offer them for deletion. The server-side threshold makes a query with no sufficiently similar memory report no match, and reranking orders the genuine matches by deeper relevance. Raise `searchThreshold` to be stricter; lower it if relevant results are missed. + +### Improved + +- **Richer feedback across every command** — results now show what actually happened: an action heading plus the relevant scope, query, match count, and affected memories. + - `/mem0-remember` echoes the stored text and the scope it landed in, instead of a generic success line. + - `/mem0-search` adds an `N matches for ""` header. + - `/mem0-forget` and `/mem0-pin` name the query when nothing matches, list the affected memory, and label the disambiguation dialog with the match count. + - `/mem0-scope` explains where new memories will be saved. +- **Cleaner `/mem0-dream`** — the consolidation protocol is now sent to the agent hidden (`display: false`, still included in LLM context) behind a concise "Dreaming…" status line, instead of dumping the raw protocol into the transcript. + ## 0.1.1 (2026-06-10) Maintenance release — no functional changes. diff --git a/integrations/pi-agent-plugin/README.md b/integrations/pi-agent-plugin/README.md index a819c464f..0c88c1b6b 100644 --- a/integrations/pi-agent-plugin/README.md +++ b/integrations/pi-agent-plugin/README.md @@ -43,6 +43,7 @@ Or create a config file at `~/.pi/agent/mem0-config.json`: "userId": "your-username", "autoCapture": true, "defaultScope": "project", + "searchThreshold": 0.2, "dream": { "enabled": true, "auto": true, @@ -55,6 +56,8 @@ Or create a config file at `~/.pi/agent/mem0-config.json`: Environment variables (`MEM0_API_KEY`, `MEM0_USER_ID`) override the config file. +`searchThreshold` (default `0.3`) is the minimum similarity score (0–1) a memory must reach to count as a match for `/mem0-search`, `/mem0-forget`, and `/mem0-pin`. It is passed to the mem0 search API (along with reranking for higher-precision ordering), so a query with no sufficiently similar memory reports no match instead of returning the closest unrelated memories. Raise it to be stricter; lower it if relevant results are missed. + ## Commands | Command | Description | diff --git a/integrations/pi-agent-plugin/package.json b/integrations/pi-agent-plugin/package.json index 7c9cbe4ae..31a3e76e0 100644 --- a/integrations/pi-agent-plugin/package.json +++ b/integrations/pi-agent-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@mem0/pi-agent-plugin", - "version": "0.1.1", + "version": "0.1.2", "type": "module", "description": "Mem0 memory extension for Pi Agent persistent, scoped, semantic memory across sessions and projects", "license": "Apache-2.0", @@ -39,8 +39,12 @@ "LICENSE" ], "pi": { - "extensions": ["./src/entry.ts"], - "skills": ["./skills"] + "extensions": [ + "./src/entry.ts" + ], + "skills": [ + "./skills" + ] }, "scripts": { "build": "tsup", @@ -49,8 +53,8 @@ "typecheck": "tsc --noEmit" }, "peerDependencies": { - "@earendil-works/pi-coding-agent": "*", "@earendil-works/pi-ai": "*", + "@earendil-works/pi-coding-agent": "*", "typebox": "*" }, "devDependencies": { @@ -63,7 +67,7 @@ "vitest": "^4.1.7" }, "dependencies": { - "mem0ai": "^3.0.6" + "mem0ai": "^3.0.7" }, "pnpm": { "overrides": { diff --git a/integrations/pi-agent-plugin/pnpm-lock.yaml b/integrations/pi-agent-plugin/pnpm-lock.yaml index 3c905352b..9ce9357d1 100644 --- a/integrations/pi-agent-plugin/pnpm-lock.yaml +++ b/integrations/pi-agent-plugin/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: .: dependencies: mem0ai: - specifier: ^3.0.6 - version: 3.0.6(@anthropic-ai/sdk@0.91.1(zod@3.25.76))(@azure/identity@4.13.1)(@azure/search-documents@12.2.0)(@cloudflare/workers-types@4.20260610.1)(@google/genai@1.52.0)(@langchain/core@1.1.48(openai@6.26.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0))(@mistralai/mistralai@2.2.1)(@qdrant/js-client-rest@1.18.0(typescript@6.0.3))(@supabase/supabase-js@2.108.1)(@types/jest@29.5.14)(@types/pg@8.11.0)(better-sqlite3@12.10.0)(cloudflare@4.5.0)(compromise@14.15.1)(groq-sdk@0.3.0)(natural@8.1.1)(ollama@0.5.18)(pg@8.11.3)(redis@4.7.1)(ws@8.21.0) + specifier: ^3.0.7 + version: 3.0.7(@anthropic-ai/sdk@0.91.1(zod@3.25.76))(@azure/identity@4.13.1)(@azure/search-documents@12.2.0)(@cloudflare/workers-types@4.20260610.1)(@google/genai@1.52.0)(@langchain/core@1.1.48(openai@6.26.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0))(@mistralai/mistralai@2.2.1)(@qdrant/js-client-rest@1.18.0(typescript@6.0.3))(@supabase/supabase-js@2.108.1)(@types/jest@29.5.14)(@types/pg@8.11.0)(better-sqlite3@12.10.0)(cloudflare@4.5.0)(compromise@14.15.1)(groq-sdk@0.3.0)(natural@8.1.1)(ollama@0.5.18)(pg@8.21.0)(redis@5.12.1)(ws@8.21.0) devDependencies: '@earendil-works/pi-ai': specifier: ^0.79.0 @@ -469,35 +469,30 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@mariozechner/clipboard-linux-arm64-musl@0.3.9': resolution: {integrity: sha512-AGuJdgKsmJdm4Pych7kv3sqe591ERRaAHW3xjLooiFzn8J+PxUyof++7YZrB5Y5tpnTO+K18Og3taj2NpluCRQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@mariozechner/clipboard-linux-riscv64-gnu@0.3.9': resolution: {integrity: sha512-DXBEAiuMpk7dhS1a9NzNxVAFi1vaKoPu7rQNgY8LIDLGrK3lnIp3nT10DUum+PKVJoJppIP+NAA8IZe4DMNDPw==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] - libc: [glibc] '@mariozechner/clipboard-linux-x64-gnu@0.3.9': resolution: {integrity: sha512-WORrMLd6EpElEME7JRKfSaY34nW1P5LbdgK5YNCS1ncG2LqmITsSMEJ8nh2mpvxb3TxqbOOKgY7k9eMJYlW9Mw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@mariozechner/clipboard-linux-x64-musl@0.3.9': resolution: {integrity: sha512-/DHn+1DrfL6oRaPPWXaOKvonFFrni666fxd+zFqiQEfvBH0tsHVWjq9iqBk0oDp0qaPA72lIMy5BptxISBEhZQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@mariozechner/clipboard-win32-arm64-msvc@0.3.9': resolution: {integrity: sha512-O5FHD3ErkMwMhNzAfu3ggy0ug4z7btZuoQgwwxlzPrwV2bxlD6WDpqBY4NCgICAgZdDKdp+loUEKVAVt8aYnhQ==} @@ -573,21 +568,12 @@ packages: resolution: {integrity: sha512-oQG/FejNpItrxRHoyctYvT3rwGZOnK4jr3JdppO/c78ktDvkWiPXPHNsrDf33K9sZdRb6PR7gi4noIapu5q4HA==} engines: {node: '>=18.0.0', pnpm: '>=8'} - '@redis/bloom@1.2.0': - resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==} - peerDependencies: - '@redis/client': ^1.0.0 - '@redis/bloom@5.12.1': resolution: {integrity: sha512-PUUfv+ms7jgPSBVoo/DN4AkPHj4D5TZSd6SbJX7egzBplkYUcKmHRE8RKia7UtZ8bSQbLguLvxVO+asKtQfZWA==} engines: {node: '>= 18.19.0'} peerDependencies: '@redis/client': ^5.12.1 - '@redis/client@1.6.1': - resolution: {integrity: sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==} - engines: {node: '>=14'} - '@redis/client@5.12.1': resolution: {integrity: sha512-7aPGWeqA3uFm43o19umzdl16CEjK/JQGtSXVPevplTaOU3VJA/rseBC1QvYUz9lLDIMBimc4SW/zrW4S89BaCA==} engines: {node: '>= 18.19.0'} @@ -600,38 +586,18 @@ packages: '@opentelemetry/api': optional: true - '@redis/graph@1.1.1': - resolution: {integrity: sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==} - peerDependencies: - '@redis/client': ^1.0.0 - - '@redis/json@1.0.7': - resolution: {integrity: sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==} - peerDependencies: - '@redis/client': ^1.0.0 - '@redis/json@5.12.1': resolution: {integrity: sha512-eOze75esLve4vfqDel7aMX08CNaiLLQS2fV8mpRN9NxPe1rVR4vQyYiW/OgtGUysF6QOr9ANhfxABKNOJfXdKg==} engines: {node: '>= 18.19.0'} peerDependencies: '@redis/client': ^5.12.1 - '@redis/search@1.2.0': - resolution: {integrity: sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==} - peerDependencies: - '@redis/client': ^1.0.0 - '@redis/search@5.12.1': resolution: {integrity: sha512-ItlxbxC9cKI6IU1TLWoczwJCRb6TdmkEpWv05UrPawqaAnWGRu3rcIqsc5vN483T2fSociuyV1UkWIL5I4//2w==} engines: {node: '>= 18.19.0'} peerDependencies: '@redis/client': ^5.12.1 - '@redis/time-series@1.1.0': - resolution: {integrity: sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==} - peerDependencies: - '@redis/client': ^1.0.0 - '@redis/time-series@5.12.1': resolution: {integrity: sha512-c6JL6E3EcZJuNqKFz+KM+l9l5mpcQiKvTwgA3blt5glWJ8hjDk0yeHN3beE/MpqYIQ8UEX44ItQzgkE/gCBELQ==} engines: {node: '>= 18.19.0'} @@ -673,42 +639,36 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.0.3': resolution: {integrity: sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - libc: [musl] '@rolldown/binding-linux-ppc64-gnu@1.0.3': resolution: {integrity: sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.0.3': resolution: {integrity: sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.0.3': resolution: {integrity: sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [glibc] '@rolldown/binding-linux-x64-musl@1.0.3': resolution: {integrity: sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - libc: [musl] '@rolldown/binding-openharmony-arm64@1.0.3': resolution: {integrity: sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==} @@ -770,79 +730,66 @@ packages: resolution: {integrity: sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.61.1': resolution: {integrity: sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.61.1': resolution: {integrity: sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.61.1': resolution: {integrity: sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.61.1': resolution: {integrity: sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.61.1': resolution: {integrity: sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.61.1': resolution: {integrity: sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.61.1': resolution: {integrity: sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.61.1': resolution: {integrity: sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.61.1': resolution: {integrity: sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.61.1': resolution: {integrity: sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.61.1': resolution: {integrity: sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.61.1': resolution: {integrity: sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.61.1': resolution: {integrity: sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==} @@ -1133,10 +1080,6 @@ packages: buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} - buffer-writer@2.0.0: - resolution: {integrity: sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==} - engines: {node: '>=4'} - buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} @@ -1427,10 +1370,6 @@ packages: resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} engines: {node: '>=18'} - generic-pool@3.9.0: - resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==} - engines: {node: '>= 4'} - get-east-asian-width@1.6.0: resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} engines: {node: '>=18'} @@ -1661,28 +1600,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -1754,8 +1689,8 @@ packages: md5@2.3.0: resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} - mem0ai@3.0.6: - resolution: {integrity: sha512-X1KbCVE5351qpJ6X2KXFhQJiW+dqoHj1otA66DSdXtIEO1uXuIdyeTKeURdAH+wjXtIcFa08ylL2meA1pBk9Iw==} + mem0ai@3.0.7: + resolution: {integrity: sha512-CUHzX7DyeKTHcI3aDsSqY9LXTD7GcFxf988796TuOa4yJgGuF2Xd2NROcBhVFRo3r9y8fVmbo3c5TF9jv1KlKw==} engines: {node: '>=18'} peerDependencies: '@anthropic-ai/sdk': ^0.40.1 @@ -1966,9 +1901,6 @@ packages: resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} engines: {node: '>=8'} - packet-reader@1.0.0: - resolution: {integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==} - partial-json@0.1.7: resolution: {integrity: sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==} @@ -2017,15 +1949,6 @@ packages: resolution: {integrity: sha512-o2XFanIMy/3+mThw69O8d4n1E5zsLhdO+OPqswezu7Z5ekP4hYDqlDjlmOpYMbzY2Br0ufCwJLdDIXeNVwcWFg==} engines: {node: '>=10'} - pg@8.11.3: - resolution: {integrity: sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==} - engines: {node: '>= 8.0.0'} - peerDependencies: - pg-native: '>=3.0.1' - peerDependenciesMeta: - pg-native: - optional: true - pg@8.21.0: resolution: {integrity: sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==} engines: {node: '>= 16.0.0'} @@ -2156,9 +2079,6 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} - redis@4.7.1: - resolution: {integrity: sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==} - redis@5.12.1: resolution: {integrity: sha512-LDsoVvb/CpoV9EN3FXvgvSHNJWuCIzl9MiO3ppOevuGLpSGJhwfQjpEwfFJcQvNSddHADDdZaWx0HnmMxRXG7g==} engines: {node: '>= 18.19.0'} @@ -2553,9 +2473,6 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} - yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yaml@2.9.0: resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} engines: {node: '>= 14.6'} @@ -3242,48 +3159,22 @@ snapshots: '@qdrant/openapi-typescript-fetch@1.2.6': {} - '@redis/bloom@1.2.0(@redis/client@1.6.1)': - dependencies: - '@redis/client': 1.6.1 - '@redis/bloom@5.12.1(@redis/client@5.12.1)': dependencies: '@redis/client': 5.12.1 - '@redis/client@1.6.1': - dependencies: - cluster-key-slot: 1.1.2 - generic-pool: 3.9.0 - yallist: 4.0.0 - '@redis/client@5.12.1': dependencies: cluster-key-slot: 1.1.2 - '@redis/graph@1.1.1(@redis/client@1.6.1)': - dependencies: - '@redis/client': 1.6.1 - - '@redis/json@1.0.7(@redis/client@1.6.1)': - dependencies: - '@redis/client': 1.6.1 - '@redis/json@5.12.1(@redis/client@5.12.1)': dependencies: '@redis/client': 5.12.1 - '@redis/search@1.2.0(@redis/client@1.6.1)': - dependencies: - '@redis/client': 1.6.1 - '@redis/search@5.12.1(@redis/client@5.12.1)': dependencies: '@redis/client': 5.12.1 - '@redis/time-series@1.1.0(@redis/client@1.6.1)': - dependencies: - '@redis/client': 1.6.1 - '@redis/time-series@5.12.1(@redis/client@5.12.1)': dependencies: '@redis/client': 5.12.1 @@ -3706,8 +3597,6 @@ snapshots: buffer-equal-constant-time@1.0.1: {} - buffer-writer@2.0.0: {} - buffer@5.7.1: dependencies: base64-js: 1.5.1 @@ -3995,8 +3884,6 @@ snapshots: transitivePeerDependencies: - supports-color - generic-pool@3.9.0: {} - get-east-asian-width@1.6.0: {} get-intrinsic@1.3.0: @@ -4303,7 +4190,7 @@ snapshots: crypt: 0.0.2 is-buffer: 1.1.6 - mem0ai@3.0.6(@anthropic-ai/sdk@0.91.1(zod@3.25.76))(@azure/identity@4.13.1)(@azure/search-documents@12.2.0)(@cloudflare/workers-types@4.20260610.1)(@google/genai@1.52.0)(@langchain/core@1.1.48(openai@6.26.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0))(@mistralai/mistralai@2.2.1)(@qdrant/js-client-rest@1.18.0(typescript@6.0.3))(@supabase/supabase-js@2.108.1)(@types/jest@29.5.14)(@types/pg@8.11.0)(better-sqlite3@12.10.0)(cloudflare@4.5.0)(compromise@14.15.1)(groq-sdk@0.3.0)(natural@8.1.1)(ollama@0.5.18)(pg@8.11.3)(redis@4.7.1)(ws@8.21.0): + mem0ai@3.0.7(@anthropic-ai/sdk@0.91.1(zod@3.25.76))(@azure/identity@4.13.1)(@azure/search-documents@12.2.0)(@cloudflare/workers-types@4.20260610.1)(@google/genai@1.52.0)(@langchain/core@1.1.48(openai@6.26.0(ws@8.21.0)(zod@3.25.76))(ws@8.21.0))(@mistralai/mistralai@2.2.1)(@qdrant/js-client-rest@1.18.0(typescript@6.0.3))(@supabase/supabase-js@2.108.1)(@types/jest@29.5.14)(@types/pg@8.11.0)(better-sqlite3@12.10.0)(cloudflare@4.5.0)(compromise@14.15.1)(groq-sdk@0.3.0)(natural@8.1.1)(ollama@0.5.18)(pg@8.21.0)(redis@5.12.1)(ws@8.21.0): dependencies: '@anthropic-ai/sdk': 0.91.1(zod@3.25.76) '@azure/identity': 4.13.1 @@ -4324,8 +4211,8 @@ snapshots: natural: 8.1.1 ollama: 0.5.18 openai: 4.104.0(ws@8.21.0)(zod@3.25.76) - pg: 8.11.3 - redis: 4.7.1 + pg: 8.21.0 + redis: 5.12.1 uuid: 14.0.0 zod: 3.25.76 transitivePeerDependencies: @@ -4515,8 +4402,6 @@ snapshots: dependencies: p-finally: 1.0.0 - packet-reader@1.0.0: {} - partial-json@0.1.7: {} path-expression-matcher@1.5.0: {} @@ -4539,10 +4424,6 @@ snapshots: pg-numeric@1.0.2: {} - pg-pool@3.14.0(pg@8.11.3): - dependencies: - pg: 8.11.3 - pg-pool@3.14.0(pg@8.21.0): dependencies: pg: 8.21.0 @@ -4567,18 +4448,6 @@ snapshots: postgres-interval: 3.0.0 postgres-range: 1.1.4 - pg@8.11.3: - dependencies: - buffer-writer: 2.0.0 - packet-reader: 1.0.0 - pg-connection-string: 2.13.0 - pg-pool: 3.14.0(pg@8.11.3) - pg-protocol: 1.14.0 - pg-types: 2.2.0 - pgpass: 1.0.5 - optionalDependencies: - pg-cloudflare: 1.4.0 - pg@8.21.0: dependencies: pg-connection-string: 2.13.0 @@ -4711,15 +4580,6 @@ snapshots: readdirp@4.1.2: {} - redis@4.7.1: - dependencies: - '@redis/bloom': 1.2.0(@redis/client@1.6.1) - '@redis/client': 1.6.1 - '@redis/graph': 1.1.1(@redis/client@1.6.1) - '@redis/json': 1.0.7(@redis/client@1.6.1) - '@redis/search': 1.2.0(@redis/client@1.6.1) - '@redis/time-series': 1.1.0(@redis/client@1.6.1) - redis@5.12.1: dependencies: '@redis/bloom': 5.12.1(@redis/client@5.12.1) @@ -5061,8 +4921,6 @@ snapshots: xtend@4.0.2: {} - yallist@4.0.0: {} - yaml@2.9.0: {} zod-to-json-schema@3.25.2(zod@4.4.3): diff --git a/integrations/pi-agent-plugin/src/commands.test.ts b/integrations/pi-agent-plugin/src/commands.test.ts index ac51ac964..1ba46c6be 100644 --- a/integrations/pi-agent-plugin/src/commands.test.ts +++ b/integrations/pi-agent-plugin/src/commands.test.ts @@ -55,6 +55,7 @@ const defaultConfig: Mem0Config = { autoCapture: false, defaultScope: "project", contextInjection: false, + searchThreshold: 0.3, dream: { enabled: false, auto: false, minHours: 24, minSessions: 5, minMemories: 20 }, }; @@ -67,6 +68,7 @@ describe("registerCommands", () => { beforeEach(() => { pi = makePi(); mem0 = makeMem0(); + defaultConfig.defaultScope = "project"; registerCommands(pi as any, mem0, defaultConfig, () => scopeCtx); }); @@ -90,11 +92,17 @@ describe("registerCommands", () => { expect(mem0.search).not.toHaveBeenCalled(); }); - it("notifies when no memories match", async () => { + it("sends a visible message naming the query when no memories match", async () => { const ctx = makeCtx(); mem0.search.mockResolvedValue({ results: [] }); await pi._invoke("mem0-forget", "old preference", ctx); - expect(ctx.ui.notify).toHaveBeenCalledWith("No matching memories found.", "info"); + expect(pi.sendMessage).toHaveBeenCalledWith( + expect.objectContaining({ + customType: "mem0-forget", + content: expect.stringContaining('No matches for "old preference"'), + display: true, + }), + ); }); it("asks for confirmation before deleting a single match", async () => { @@ -111,6 +119,22 @@ describe("registerCommands", () => { expect(mem0.delete).toHaveBeenCalledWith("abc-123"); }); + it("sends a visible confirmation showing what was forgotten", async () => { + const ctx = makeCtx(true); + mem0.search.mockResolvedValue({ results: [{ id: "abc-123", memory: "test mem" }] }); + mem0.delete.mockResolvedValue({ message: "Deleted" }); + + await pi._invoke("mem0-forget", "test", ctx); + + expect(pi.sendMessage).toHaveBeenCalledWith( + expect.objectContaining({ + customType: "mem0-forget", + content: expect.stringContaining("Forgotten"), + display: true, + }), + ); + }); + it("does not delete when user cancels confirmation", async () => { const ctx = makeCtx(false); mem0.search.mockResolvedValue({ results: [{ id: "abc-123", memory: "test mem" }] }); @@ -119,7 +143,9 @@ describe("registerCommands", () => { expect(ctx.ui.confirm).toHaveBeenCalled(); expect(mem0.delete).not.toHaveBeenCalled(); - expect(ctx.ui.notify).toHaveBeenCalledWith("Cancelled.", "info"); + expect(pi.sendMessage).toHaveBeenCalledWith( + expect.objectContaining({ content: expect.stringContaining("Cancelled"), display: true }), + ); }); it("uses select UI for multiple matches and deletes chosen memory", async () => { @@ -136,13 +162,20 @@ describe("registerCommands", () => { await pi._invoke("mem0-forget", "test", ctx); expect(ctx.ui.select).toHaveBeenCalledWith( - "Which memory should I delete?", + expect.stringContaining("which should I delete"), expect.arrayContaining([ expect.stringContaining("mem one"), expect.stringContaining("mem two"), ]), ); expect(mem0.delete).toHaveBeenCalledWith("id-2"); + expect(pi.sendMessage).toHaveBeenCalledWith( + expect.objectContaining({ + customType: "mem0-forget", + content: expect.stringContaining("Forgotten"), + display: true, + }), + ); }); it("does not delete when user cancels select", async () => { @@ -158,7 +191,9 @@ describe("registerCommands", () => { await pi._invoke("mem0-forget", "test", ctx); expect(mem0.delete).not.toHaveBeenCalled(); - expect(ctx.ui.notify).toHaveBeenCalledWith("Cancelled.", "info"); + expect(pi.sendMessage).toHaveBeenCalledWith( + expect.objectContaining({ content: expect.stringContaining("Cancelled"), display: true }), + ); }); }); @@ -179,6 +214,22 @@ describe("registerCommands", () => { expect(mem0.delete).not.toHaveBeenCalled(); }); + it("sends a visible confirmation after pinning", async () => { + const ctx = makeCtx(true); + mem0.search.mockResolvedValue({ results: [{ id: "abc-123", memory: "important fact" }] }); + mem0.update.mockResolvedValue([]); + + await pi._invoke("mem0-pin", "important", ctx); + + expect(pi.sendMessage).toHaveBeenCalledWith( + expect.objectContaining({ + customType: "mem0-pin", + content: expect.stringContaining("Pinned"), + display: true, + }), + ); + }); + it("does not pin when user cancels", async () => { const ctx = makeCtx(false); mem0.search.mockResolvedValue({ results: [{ id: "abc-123", memory: "fact" }] }); @@ -188,7 +239,7 @@ describe("registerCommands", () => { expect(mem0.update).not.toHaveBeenCalled(); }); - it("skips already-pinned memories", async () => { + it("skips already-pinned memories with a visible message", async () => { const ctx = makeCtx(); mem0.search.mockResolvedValue({ results: [{ id: "abc-123", memory: "[PINNED] fact" }] }); @@ -196,7 +247,9 @@ describe("registerCommands", () => { expect(ctx.ui.confirm).not.toHaveBeenCalled(); expect(mem0.add).not.toHaveBeenCalled(); - expect(ctx.ui.notify).toHaveBeenCalledWith("Already pinned.", "info"); + expect(pi.sendMessage).toHaveBeenCalledWith( + expect.objectContaining({ content: expect.stringContaining("Already pinned"), display: true }), + ); }); it("uses select UI for multiple matches and pins chosen memory", async () => { @@ -213,7 +266,7 @@ describe("registerCommands", () => { await pi._invoke("mem0-pin", "fact", ctx); expect(ctx.ui.select).toHaveBeenCalledWith( - "Which memory should I pin?", + expect.stringContaining("which should I pin"), expect.arrayContaining([ expect.stringContaining("fact one"), expect.stringContaining("fact two"), @@ -235,18 +288,23 @@ describe("registerCommands", () => { await pi._invoke("mem0-pin", "fact", ctx); expect(mem0.update).not.toHaveBeenCalled(); - expect(ctx.ui.notify).toHaveBeenCalledWith("Cancelled.", "info"); + expect(pi.sendMessage).toHaveBeenCalledWith( + expect.objectContaining({ content: expect.stringContaining("Cancelled"), display: true }), + ); }); }); describe("/mem0-search", () => { - it("always performs semantic search", async () => { + it("performs server-side semantic search with a relevance threshold", async () => { const ctx = makeCtx(); mem0.search.mockResolvedValue({ results: [{ id: "id-1", memory: "result" }] }); await pi._invoke("mem0-search", "my preferences", ctx); - expect(mem0.search).toHaveBeenCalledWith("my preferences", expect.any(Object)); + expect(mem0.search).toHaveBeenCalledWith( + "my preferences", + expect.objectContaining({ threshold: 0.3, topK: 10, rerank: true }), + ); expect(pi.sendMessage).toHaveBeenCalledWith( expect.objectContaining({ customType: "mem0-search" }), ); @@ -263,16 +321,49 @@ describe("registerCommands", () => { expect(mem0.get).not.toHaveBeenCalled(); }); - it("shows empty results message", async () => { + it("shows a no-matches message naming the query", async () => { const ctx = makeCtx(); mem0.search.mockResolvedValue({ results: [] }); await pi._invoke("mem0-search", "nonexistent", ctx); expect(pi.sendMessage).toHaveBeenCalledWith( - expect.objectContaining({ content: "No memories found." }), + expect.objectContaining({ content: expect.stringContaining("No matches") }), ); }); + + it("shows a result count header when there are matches", async () => { + const ctx = makeCtx(); + mem0.search.mockResolvedValue({ + results: [ + { id: "id-1", memory: "one" }, + { id: "id-2", memory: "two" }, + ], + }); + + await pi._invoke("mem0-search", "stuff", ctx); + + expect(pi.sendMessage).toHaveBeenCalledWith( + expect.objectContaining({ content: expect.stringContaining("2 matches") }), + ); + }); + + it("shows all results the API returns (relevance gating is server-side)", async () => { + const ctx = makeCtx(); + mem0.search.mockResolvedValue({ + results: [ + { id: "id-1", memory: "first match", score: 0.62 }, + { id: "id-2", memory: "second match", score: 0.31 }, + ], + }); + + await pi._invoke("mem0-search", "stuff", ctx); + + const call = pi.sendMessage.mock.calls.find(([m]: any[]) => m.customType === "mem0-search"); + expect(call?.[0].content).toContain("first match"); + expect(call?.[0].content).toContain("second match"); + expect(call?.[0].content).toContain("2 matches"); + }); }); describe("/mem0-remember", () => { @@ -288,10 +379,132 @@ describe("registerCommands", () => { ); }); + it("shows the stored text in a visible confirmation (infer:false status response)", async () => { + const ctx = makeCtx(); + mem0.add.mockResolvedValue({ message: "Memories stored successfully" }); + + await pi._invoke("mem0-remember", "I prefer dark mode", ctx); + + expect(pi.sendMessage).toHaveBeenCalledWith( + expect.objectContaining({ + customType: "mem0-remember", + content: expect.stringContaining("I prefer dark mode"), + display: true, + }), + ); + }); + + it("lists memory objects returned by the API when present", async () => { + const ctx = makeCtx(); + mem0.add.mockResolvedValue([{ id: "m1", memory: "Uses dark mode", event: "ADD" }]); + + await pi._invoke("mem0-remember", "I prefer dark mode", ctx); + + expect(pi.sendMessage).toHaveBeenCalledWith( + expect.objectContaining({ + customType: "mem0-remember", + content: expect.stringContaining("Uses dark mode"), + display: true, + }), + ); + }); + it("shows warning when no text provided", async () => { const ctx = makeCtx(); await pi._invoke("mem0-remember", " ", ctx); expect(ctx.ui.notify).toHaveBeenCalledWith("Usage: /mem0-remember ", "warning"); }); }); + + describe("/mem0-scope", () => { + it("sends a visible message showing the current scope when no arg is given", async () => { + const ctx = makeCtx(); + await pi._invoke("mem0-scope", "", ctx); + expect(pi.sendMessage).toHaveBeenCalledWith( + expect.objectContaining({ + customType: "mem0-scope", + content: expect.stringContaining("Current scope:"), + display: true, + }), + ); + }); + + it("sends a visible confirmation after changing scope", async () => { + const ctx = makeCtx(); + await pi._invoke("mem0-scope", "global", ctx); + expect(pi.sendMessage).toHaveBeenCalledWith( + expect.objectContaining({ + customType: "mem0-scope", + content: expect.stringContaining("Scope changed to global"), + display: true, + }), + ); + }); + + it("warns on an invalid scope", async () => { + const ctx = makeCtx(); + await pi._invoke("mem0-scope", "bogus", ctx); + expect(ctx.ui.notify).toHaveBeenCalledWith( + expect.stringContaining('Invalid scope "bogus"'), + "warning", + ); + }); + }); + + describe("/mem0-tour", () => { + it("shows an empty-state message when there are no memories", async () => { + const ctx = makeCtx(); + mem0.getAll.mockResolvedValue({ results: [] }); + + await pi._invoke("mem0-tour", "", ctx); + + expect(pi.sendMessage).toHaveBeenCalledWith( + expect.objectContaining({ + customType: "mem0-tour", + content: expect.stringContaining("No memories"), + display: true, + }), + ); + }); + + it("groups memories by category with a count header", async () => { + const ctx = makeCtx(); + mem0.getAll.mockResolvedValue({ + results: [ + { id: "id-1", memory: "likes tea", categories: ["preferences"] }, + { id: "id-2", memory: "uses vim", categories: ["technical"] }, + ], + }); + + await pi._invoke("mem0-tour", "", ctx); + + expect(pi.sendMessage).toHaveBeenCalledWith( + expect.objectContaining({ + customType: "mem0-tour", + content: expect.stringContaining("Memory tour"), + display: true, + }), + ); + }); + }); + + describe("/mem0-dream", () => { + it("feeds the protocol to the agent and shows a clean status line", async () => { + const ctx = makeCtx(); + + await pi._invoke("mem0-dream", "", ctx); + + expect(pi.sendMessage).toHaveBeenCalledWith( + expect.objectContaining({ customType: "mem0-dream", display: false }), + expect.objectContaining({ triggerTurn: true }), + ); + expect(pi.sendMessage).toHaveBeenCalledWith( + expect.objectContaining({ + customType: "mem0-dream", + content: expect.stringContaining("Dreaming"), + display: true, + }), + ); + }); + }); }); diff --git a/integrations/pi-agent-plugin/src/commands.ts b/integrations/pi-agent-plugin/src/commands.ts index 196ac0ebf..e5b760043 100644 --- a/integrations/pi-agent-plugin/src/commands.ts +++ b/integrations/pi-agent-plugin/src/commands.ts @@ -9,6 +9,8 @@ import { acquireDreamLock } from "./dream/index.ts"; import { CONFIG_DIR } from "./config/index.ts"; import { captureCommandEvent } from "./telemetry.ts"; +const SEARCH_TOP_K = 10; + export function registerCommands( pi: ExtensionAPI, mem0: MemoryClient, @@ -16,7 +18,24 @@ export function registerCommands( getScopeCtx: () => ScopeContext, telemetryCtx?: { apiKey?: string }, ): void { - // ── /mem0-remember ────────────────────────────────────────────────── + const sendFeedback = (customType: string, content: string): void => { + pi.sendMessage({ customType, content, display: true }); + }; + + const pluralize = (n: number, one: string, many: string): string => + `${n} ${n === 1 ? one : many}`; + + const searchMemories = async (query: string, scope: Scope) => { + const filters = resolveSearchFilters(scope, getScopeCtx()); + const result = await mem0.search(query, { + filters, + threshold: config.searchThreshold, + topK: SEARCH_TOP_K, + rerank: true, + }); + return result.results ?? []; + }; + pi.registerCommand("mem0-remember", { description: "Store a memory verbatim (no inference)", handler: async (args, ctx) => { @@ -26,19 +45,24 @@ export function registerCommands( return; } - const scopeCtx = getScopeCtx(); - const addParams = resolveAddParams(config.defaultScope, scopeCtx); + const addParams = resolveAddParams(config.defaultScope, getScopeCtx()); const result = await mem0.add( [{ role: "user", content: text }], { ...addParams, customCategories: DEFAULT_CUSTOM_CATEGORIES, infer: false }, ); - const msg = (result as { message?: string }).message ?? "Memory stored."; captureCommandEvent("mem0-remember", {}, telemetryCtx); - ctx.ui.notify(msg, "info"); + + const storedItems = (Array.isArray(result) ? result : []) + .map((m) => (m as { memory?: string }).memory) + .filter((m): m is string => Boolean(m)); + const items = storedItems.length > 0 ? storedItems : [text]; + sendFeedback( + "mem0-remember", + [`**Stored to ${config.defaultScope} memory**`, ...items.map((m) => `- ${m}`)].join("\n"), + ); }, }); - // ── /mem0-forget ──────────────────────────────────────────────────── pi.registerCommand("mem0-forget", { description: "Delete memories matching a natural language query", handler: async (args, ctx) => { @@ -48,49 +72,51 @@ export function registerCommands( return; } - const scopeCtx = getScopeCtx(); - const filters = resolveSearchFilters(config.defaultScope, scopeCtx); - const result = await mem0.search(query, { filters }); - const memories = result.results ?? []; + const memories = await searchMemories(query, config.defaultScope); if (memories.length === 0) { captureCommandEvent("mem0-forget", { result_count: 0 }, telemetryCtx); - ctx.ui.notify("No matching memories found.", "info"); + sendFeedback("mem0-forget", `**No matches for "${query}"** — nothing to forget.`); return; } + const forgotten = (mem: Parameters[0]) => { + captureCommandEvent("mem0-forget", { deleted_count: 1 }, telemetryCtx); + sendFeedback( + "mem0-forget", + [`**Forgotten from ${config.defaultScope} memory**`, `- ${formatMemoryCompact(mem)}`].join("\n"), + ); + }; + if (memories.length === 1) { const target = memories[0]; - const confirmed = await ctx.ui.confirm( - "Delete this memory?", - formatMemoryCompact(target), - ); + const confirmed = await ctx.ui.confirm("Delete this memory?", formatMemoryCompact(target)); if (!confirmed) { - ctx.ui.notify("Cancelled.", "info"); + sendFeedback("mem0-forget", "**Cancelled** — no memories deleted."); return; } await mem0.delete(target.id); - captureCommandEvent("mem0-forget", { deleted_count: 1 }, telemetryCtx); - ctx.ui.notify(`Deleted: ${formatMemoryCompact(target)}`, "info"); + forgotten(target); return; } const labels = memories.map((m) => formatMemoryCompact(m)); - const selected = await ctx.ui.select("Which memory should I delete?", labels); + const selected = await ctx.ui.select( + `Found ${pluralize(memories.length, "match", "matches")} for "${query}" — which should I delete?`, + labels, + ); if (!selected) { - ctx.ui.notify("Cancelled.", "info"); + sendFeedback("mem0-forget", "**Cancelled** — no memories deleted."); return; } const idx = labels.indexOf(selected); if (idx < 0) return; const target = memories[idx]; await mem0.delete(target.id); - captureCommandEvent("mem0-forget", { deleted_count: 1 }, telemetryCtx); - ctx.ui.notify(`Deleted: ${formatMemoryCompact(target)}`, "info"); + forgotten(target); }, }); - // ── /mem0-search ──────────────────────────────────────────────────── pi.registerCommand("mem0-search", { description: "Semantic search across memories", handler: async (args, ctx) => { @@ -100,21 +126,25 @@ export function registerCommands( return; } - const scopeCtx = getScopeCtx(); - const filters = resolveSearchFilters(config.defaultScope, scopeCtx); - const result = await mem0.search(query, { filters }); - const memories = result.results ?? []; - + const memories = await searchMemories(query, config.defaultScope); captureCommandEvent("mem0-search", { result_count: memories.length }, telemetryCtx); - pi.sendMessage({ - customType: "mem0-search", - content: formatMemoryList(memories), - display: true, - }); + + if (memories.length === 0) { + sendFeedback("mem0-search", `**No matches for "${query}"** · ${config.defaultScope} scope`); + return; + } + + sendFeedback( + "mem0-search", + [ + `**${pluralize(memories.length, "match", "matches")} for "${query}"** · ${config.defaultScope} scope`, + "", + formatMemoryList(memories), + ].join("\n"), + ); }, }); - // ── /mem0-tour ────────────────────────────────────────────────────── pi.registerCommand("mem0-tour", { description: "Browse all memories grouped by category", handler: async (args, ctx) => { @@ -125,19 +155,21 @@ export function registerCommands( return; } const scope: Scope = (raw as Scope) || config.defaultScope; - const scopeCtx = getScopeCtx(); - const filters = resolveSearchFilters(scope, scopeCtx); + const filters = resolveSearchFilters(scope, getScopeCtx()); const result = await mem0.getAll({ filters }); const memories = result.results ?? []; if (memories.length === 0) { captureCommandEvent("mem0-tour", { memory_count: 0, scope }, telemetryCtx); - pi.sendMessage({ customType: "mem0-tour", content: "No memories found.", display: true }); + sendFeedback("mem0-tour", `**No memories in ${scope} scope yet** — store one with \`/mem0-remember\`.`); return; } const groups = groupByCategory(memories); - const lines: string[] = [`**Memory Tour** (${memories.length} total, scope: ${scope})`, ""]; + const lines: string[] = [ + `**Memory tour** · ${pluralize(memories.length, "memory", "memories")} · ${scope} scope`, + "", + ]; for (const [category, items] of groups) { lines.push(`### ${category} (${items.length})`); @@ -148,11 +180,10 @@ export function registerCommands( } captureCommandEvent("mem0-tour", { memory_count: memories.length, scope }, telemetryCtx); - pi.sendMessage({ customType: "mem0-tour", content: lines.join("\n"), display: true }); + sendFeedback("mem0-tour", lines.join("\n")); }, }); - // ── /mem0-dream ───────────────────────────────────────────────────── pi.registerCommand("mem0-dream", { description: "Consolidate memories — merge duplicates, prune stale entries, resolve contradictions", handler: async (_args, ctx) => { @@ -162,12 +193,14 @@ export function registerCommands( } captureCommandEvent("mem0-dream", {}, telemetryCtx); - pi.sendMessage({ customType: "mem0-dream", content: DREAM_PROTOCOL, display: true }, { triggerTurn: true }); - ctx.ui.notify("Dream consolidation started.", "info"); + pi.sendMessage({ customType: "mem0-dream", content: DREAM_PROTOCOL, display: false }, { triggerTurn: true }); + sendFeedback( + "mem0-dream", + "**Dreaming** — reviewing your memories to merge duplicates, resolve contradictions, and prune stale entries. I'll report what changed.", + ); }, }); - // ── /mem0-pin ─────────────────────────────────────────────────────── pi.registerCommand("mem0-pin", { description: "Pin a memory to protect it from dream pruning", handler: async (args, ctx) => { @@ -177,42 +210,49 @@ export function registerCommands( return; } - const scopeCtx = getScopeCtx(); - const filters = resolveSearchFilters(config.defaultScope, scopeCtx); - const result = await mem0.search(query, { filters }); - const memories = result.results ?? []; + const memories = await searchMemories(query, config.defaultScope); if (memories.length === 0) { captureCommandEvent("mem0-pin", { result_count: 0 }, telemetryCtx); - ctx.ui.notify("No matching memories found to pin.", "info"); + sendFeedback("mem0-pin", `**No matches for "${query}"** — nothing to pin.`); return; } + const pinned = (mem: Parameters[0]) => { + captureCommandEvent("mem0-pin", { pinned: true }, telemetryCtx); + sendFeedback( + "mem0-pin", + ["**Pinned** — protected from dream pruning", `- ${formatMemoryCompact(mem)}`].join("\n"), + ); + }; + const alreadyPinned = (mem: Parameters[0]) => { + sendFeedback("mem0-pin", ["**Already pinned**", `- ${formatMemoryCompact(mem)}`].join("\n")); + }; + if (memories.length === 1) { const target = memories[0]; const text = target.memory ?? ""; if (text.startsWith("[PINNED]")) { - ctx.ui.notify("Already pinned.", "info"); + alreadyPinned(target); return; } - const confirmed = await ctx.ui.confirm( - "Pin this memory?", - formatMemoryCompact(target), - ); + const confirmed = await ctx.ui.confirm("Pin this memory?", formatMemoryCompact(target)); if (!confirmed) { - ctx.ui.notify("Cancelled.", "info"); + sendFeedback("mem0-pin", "**Cancelled** — nothing was pinned."); return; } await mem0.update(target.id, { text: `[PINNED] ${text}` }); - captureCommandEvent("mem0-pin", { pinned: true }, telemetryCtx); - ctx.ui.notify(`Pinned: ${formatMemoryCompact(target)}`, "info"); + pinned(target); return; } const labels = memories.map((m) => formatMemoryCompact(m)); - const selected = await ctx.ui.select("Which memory should I pin?", labels); + const selected = await ctx.ui.select( + `Found ${pluralize(memories.length, "match", "matches")} for "${query}" — which should I pin?`, + labels, + ); if (!selected) { - ctx.ui.notify("Cancelled.", "info"); + sendFeedback("mem0-pin", "**Cancelled** — nothing was pinned."); return; } const idx = labels.indexOf(selected); @@ -220,16 +260,14 @@ export function registerCommands( const target = memories[idx]; const selectedText = target.memory ?? ""; if (selectedText.startsWith("[PINNED]")) { - ctx.ui.notify("Already pinned.", "info"); + alreadyPinned(target); return; } await mem0.update(target.id, { text: `[PINNED] ${selectedText}` }); - captureCommandEvent("mem0-pin", { pinned: true }, telemetryCtx); - ctx.ui.notify(`Pinned: ${formatMemoryCompact(target)}`, "info"); + pinned(target); }, }); - // ── /mem0-scope ───────────────────────────────────────────────────── pi.registerCommand("mem0-scope", { description: "Change default memory scope for this session (project, session, global)", handler: async (args, ctx) => { @@ -237,7 +275,13 @@ export function registerCommands( const valid: Scope[] = ["project", "session", "global"]; if (!scope) { - ctx.ui.notify(`Current scope: ${config.defaultScope}. Usage: /mem0-scope <${valid.join("|")}>`, "info"); + sendFeedback( + "mem0-scope", + [ + `**Current scope: ${config.defaultScope}**`, + `New memories save to the **${config.defaultScope}** pool. Switch with \`/mem0-scope <${valid.join(" | ")}>\`.`, + ].join("\n"), + ); return; } @@ -248,11 +292,16 @@ export function registerCommands( config.defaultScope = scope as Scope; captureCommandEvent("mem0-scope", { scope }, telemetryCtx); - ctx.ui.notify(`Default scope changed to "${scope}" for this session.`, "info"); + sendFeedback( + "mem0-scope", + [ + `**Scope changed to ${scope}**`, + `New memories now save to the **${scope}** pool for this session.`, + ].join("\n"), + ); }, }); - // ── /mem0-status ──────────────────────────────────────────────────── pi.registerCommand("mem0-status", { description: "Show connection health, identity, project, and memory count", handler: async (_args, _ctx) => { @@ -270,22 +319,21 @@ export function registerCommands( } const lines = [ - "**Mem0 Status**", + "**Mem0 status**", "", `- Connection: ${connected ? "connected" : "disconnected"}`, `- User: ${scopeCtx.userId}`, `- Project: ${scopeCtx.appId}`, `- Session: ${scopeCtx.runId}`, `- Default scope: ${config.defaultScope}`, + `- Search relevance threshold: ${config.searchThreshold}`, `- Project memories: ${count}`, `- Auto-capture: ${config.autoCapture ? "on" : "off"}`, `- Dream: ${config.dream.enabled ? "enabled" : "disabled"}`, ]; captureCommandEvent("mem0-status", { connected, memory_count: count }, telemetryCtx); - pi.sendMessage({ customType: "mem0-status", content: lines.join("\n"), display: true }); + sendFeedback("mem0-status", lines.join("\n")); }, }); - - } diff --git a/integrations/pi-agent-plugin/src/config/index.ts b/integrations/pi-agent-plugin/src/config/index.ts index d01a412fa..3031cbb11 100644 --- a/integrations/pi-agent-plugin/src/config/index.ts +++ b/integrations/pi-agent-plugin/src/config/index.ts @@ -21,6 +21,7 @@ const DEFAULT_CONFIG: Mem0Config = { autoCapture: true, defaultScope: "project", contextInjection: false, + searchThreshold: 0.3, dream: DEFAULT_DREAM, }; diff --git a/integrations/pi-agent-plugin/src/types.ts b/integrations/pi-agent-plugin/src/types.ts index 482028c55..e23e80e4c 100644 --- a/integrations/pi-agent-plugin/src/types.ts +++ b/integrations/pi-agent-plugin/src/types.ts @@ -14,6 +14,7 @@ export interface Mem0Config { autoCapture: boolean; defaultScope: Scope; contextInjection: boolean; + searchThreshold: number; dream: DreamConfig; }