Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fabc87f6e | ||
|
|
7900a5d8d1 | ||
|
|
cbf97c6077 | ||
|
|
44c6a07ddf | ||
|
|
6f207d2e28 | ||
|
|
c05556f3cf | ||
|
|
0df3e4b87d | ||
|
|
b51f7692f0 | ||
|
|
dc7f88363f | ||
|
|
c7ee362aff | ||
|
|
d873892dad |
@@ -32,6 +32,9 @@ jobs:
|
||||
- name: Type check
|
||||
run: bun run type-check
|
||||
|
||||
- name: Test
|
||||
run: bun test
|
||||
|
||||
- name: Build
|
||||
run: bun run build
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"tsup": "^8.0.0",
|
||||
"tsx": "^4.7.0",
|
||||
"vite": "^6.0.0",
|
||||
"vitest": "^4.1.0",
|
||||
"vitest": "^4.1.11",
|
||||
"@biomejs/biome": "^1.7.0",
|
||||
"@types/node": "^20.0.0"
|
||||
},
|
||||
|
||||
Generated
+46
-46
@@ -51,8 +51,8 @@ importers:
|
||||
specifier: ^6.0.0
|
||||
version: 6.4.3(@types/node@20.19.37)(tsx@4.21.0)
|
||||
vitest:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.8(@types/node@20.19.37)(vite@6.4.3(@types/node@20.19.37)(tsx@4.21.0))
|
||||
specifier: ^4.1.11
|
||||
version: 4.1.11(@types/node@20.19.37)(vite@6.4.3(@types/node@20.19.37)(tsx@4.21.0))
|
||||
|
||||
packages:
|
||||
|
||||
@@ -439,11 +439,11 @@ packages:
|
||||
'@types/node@20.19.37':
|
||||
resolution: {integrity: sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==}
|
||||
|
||||
'@vitest/expect@4.1.8':
|
||||
resolution: {integrity: sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==}
|
||||
'@vitest/expect@4.1.11':
|
||||
resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==}
|
||||
|
||||
'@vitest/mocker@4.1.8':
|
||||
resolution: {integrity: sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==}
|
||||
'@vitest/mocker@4.1.11':
|
||||
resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==}
|
||||
peerDependencies:
|
||||
msw: ^2.4.9
|
||||
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
@@ -453,20 +453,20 @@ packages:
|
||||
vite:
|
||||
optional: true
|
||||
|
||||
'@vitest/pretty-format@4.1.8':
|
||||
resolution: {integrity: sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==}
|
||||
'@vitest/pretty-format@4.1.11':
|
||||
resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==}
|
||||
|
||||
'@vitest/runner@4.1.8':
|
||||
resolution: {integrity: sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==}
|
||||
'@vitest/runner@4.1.11':
|
||||
resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==}
|
||||
|
||||
'@vitest/snapshot@4.1.8':
|
||||
resolution: {integrity: sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==}
|
||||
'@vitest/snapshot@4.1.11':
|
||||
resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==}
|
||||
|
||||
'@vitest/spy@4.1.8':
|
||||
resolution: {integrity: sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==}
|
||||
'@vitest/spy@4.1.11':
|
||||
resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==}
|
||||
|
||||
'@vitest/utils@4.1.8':
|
||||
resolution: {integrity: sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==}
|
||||
'@vitest/utils@4.1.11':
|
||||
resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==}
|
||||
|
||||
acorn@8.16.0:
|
||||
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
|
||||
@@ -910,20 +910,20 @@ packages:
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
vitest@4.1.8:
|
||||
resolution: {integrity: sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==}
|
||||
vitest@4.1.11:
|
||||
resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==}
|
||||
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@edge-runtime/vm': '*'
|
||||
'@opentelemetry/api': ^1.9.0
|
||||
'@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
|
||||
'@vitest/browser-playwright': 4.1.8
|
||||
'@vitest/browser-preview': 4.1.8
|
||||
'@vitest/browser-webdriverio': 4.1.8
|
||||
'@vitest/coverage-istanbul': 4.1.8
|
||||
'@vitest/coverage-v8': 4.1.8
|
||||
'@vitest/ui': 4.1.8
|
||||
'@vitest/browser-playwright': 4.1.11
|
||||
'@vitest/browser-preview': 4.1.11
|
||||
'@vitest/browser-webdriverio': 4.1.11
|
||||
'@vitest/coverage-istanbul': 4.1.11
|
||||
'@vitest/coverage-v8': 4.1.11
|
||||
'@vitest/ui': 4.1.11
|
||||
happy-dom: '*'
|
||||
jsdom: '*'
|
||||
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
@@ -1186,44 +1186,44 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@vitest/expect@4.1.8':
|
||||
'@vitest/expect@4.1.11':
|
||||
dependencies:
|
||||
'@standard-schema/spec': 1.1.0
|
||||
'@types/chai': 5.2.3
|
||||
'@vitest/spy': 4.1.8
|
||||
'@vitest/utils': 4.1.8
|
||||
'@vitest/spy': 4.1.11
|
||||
'@vitest/utils': 4.1.11
|
||||
chai: 6.2.2
|
||||
tinyrainbow: 3.1.0
|
||||
|
||||
'@vitest/mocker@4.1.8(vite@6.4.3(@types/node@20.19.37)(tsx@4.21.0))':
|
||||
'@vitest/mocker@4.1.11(vite@6.4.3(@types/node@20.19.37)(tsx@4.21.0))':
|
||||
dependencies:
|
||||
'@vitest/spy': 4.1.8
|
||||
'@vitest/spy': 4.1.11
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 6.4.3(@types/node@20.19.37)(tsx@4.21.0)
|
||||
|
||||
'@vitest/pretty-format@4.1.8':
|
||||
'@vitest/pretty-format@4.1.11':
|
||||
dependencies:
|
||||
tinyrainbow: 3.1.0
|
||||
|
||||
'@vitest/runner@4.1.8':
|
||||
'@vitest/runner@4.1.11':
|
||||
dependencies:
|
||||
'@vitest/utils': 4.1.8
|
||||
'@vitest/utils': 4.1.11
|
||||
pathe: 2.0.3
|
||||
|
||||
'@vitest/snapshot@4.1.8':
|
||||
'@vitest/snapshot@4.1.11':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 4.1.8
|
||||
'@vitest/utils': 4.1.8
|
||||
'@vitest/pretty-format': 4.1.11
|
||||
'@vitest/utils': 4.1.11
|
||||
magic-string: 0.30.21
|
||||
pathe: 2.0.3
|
||||
|
||||
'@vitest/spy@4.1.8': {}
|
||||
'@vitest/spy@4.1.11': {}
|
||||
|
||||
'@vitest/utils@4.1.8':
|
||||
'@vitest/utils@4.1.11':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 4.1.8
|
||||
'@vitest/pretty-format': 4.1.11
|
||||
convert-source-map: 2.0.0
|
||||
tinyrainbow: 3.1.0
|
||||
|
||||
@@ -1627,15 +1627,15 @@ snapshots:
|
||||
fsevents: 2.3.3
|
||||
tsx: 4.21.0
|
||||
|
||||
vitest@4.1.8(@types/node@20.19.37)(vite@6.4.3(@types/node@20.19.37)(tsx@4.21.0)):
|
||||
vitest@4.1.11(@types/node@20.19.37)(vite@6.4.3(@types/node@20.19.37)(tsx@4.21.0)):
|
||||
dependencies:
|
||||
'@vitest/expect': 4.1.8
|
||||
'@vitest/mocker': 4.1.8(vite@6.4.3(@types/node@20.19.37)(tsx@4.21.0))
|
||||
'@vitest/pretty-format': 4.1.8
|
||||
'@vitest/runner': 4.1.8
|
||||
'@vitest/snapshot': 4.1.8
|
||||
'@vitest/spy': 4.1.8
|
||||
'@vitest/utils': 4.1.8
|
||||
'@vitest/expect': 4.1.11
|
||||
'@vitest/mocker': 4.1.11(vite@6.4.3(@types/node@20.19.37)(tsx@4.21.0))
|
||||
'@vitest/pretty-format': 4.1.11
|
||||
'@vitest/runner': 4.1.11
|
||||
'@vitest/snapshot': 4.1.11
|
||||
'@vitest/spy': 4.1.11
|
||||
'@vitest/utils': 4.1.11
|
||||
es-module-lexer: 2.1.0
|
||||
expect-type: 1.3.0
|
||||
magic-string: 0.30.21
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: "Overview"
|
||||
seo:
|
||||
title: "API Reference Overview - Mem0"
|
||||
title: "API Reference Overview"
|
||||
sidebarTitle: "Overview"
|
||||
icon: "terminal"
|
||||
iconType: "solid"
|
||||
description: "REST APIs for memory management, search, and entity operations"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: 'Delete Memory'
|
||||
seo:
|
||||
title: "Delete Memory API Endpoint - Mem0"
|
||||
title: "Delete Memory API Endpoint"
|
||||
sidebarTitle: "Delete Memory"
|
||||
description: "Delete a single memory by its unique memory ID from the Mem0 platform using the DELETE endpoint."
|
||||
openapi: delete /v1/memories/{memory_id}/
|
||||
---
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: 'Update Memory'
|
||||
seo:
|
||||
title: "Update Memory API Endpoint - Mem0"
|
||||
title: "Update Memory API Endpoint"
|
||||
sidebarTitle: "Update Memory"
|
||||
description: "Update the content, metadata, timestamp, or expiration date of a single memory by its unique ID using the PUT endpoint."
|
||||
openapi: put /v1/memories/{memory_id}/
|
||||
---
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: 'Add Member'
|
||||
seo:
|
||||
title: "Add Organization Member API Endpoint - Mem0"
|
||||
title: "Add Organization Member API Endpoint"
|
||||
sidebarTitle: "Add Member"
|
||||
description: "Add a new member to an organization with a specified role such as READER or OWNER access level."
|
||||
openapi: post /api/v1/orgs/organizations/{org_id}/members/
|
||||
---
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: 'Get Members'
|
||||
seo:
|
||||
title: "Get Organization Members API Endpoint - Mem0"
|
||||
title: "Get Organization Members API Endpoint"
|
||||
sidebarTitle: "Get Members"
|
||||
description: "Retrieve a list of all members belonging to a specific organization on the Mem0 platform."
|
||||
openapi: get /api/v1/orgs/organizations/{org_id}/members/
|
||||
---
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: 'Add Member'
|
||||
seo:
|
||||
title: "Add Project Member API Endpoint - Mem0"
|
||||
title: "Add Project Member API Endpoint"
|
||||
sidebarTitle: "Add Member"
|
||||
description: "Add a new member to a project with a specified role such as READER or OWNER access level."
|
||||
openapi: post /api/v1/orgs/organizations/{org_id}/projects/{project_id}/members/
|
||||
---
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: 'Get Members'
|
||||
seo:
|
||||
title: "Get Project Members API Endpoint - Mem0"
|
||||
title: "Get Project Members API Endpoint"
|
||||
sidebarTitle: "Get Members"
|
||||
description: "Retrieve a list of all members belonging to a specific project on the Mem0 platform."
|
||||
openapi: get /api/v1/orgs/organizations/{org_id}/projects/{project_id}/members/
|
||||
---
|
||||
@@ -2371,6 +2371,12 @@ Initial release of the Mem0 plugin for Claude Code and Cursor, followed by Codex
|
||||
|
||||
<Tab title="Claude Code">
|
||||
|
||||
<Update label="Unreleased" description="Sidekick availability">
|
||||
|
||||
Sidekick is now available only in Claude Code, with Sonnet, worktree isolation, and parent memories.
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-09-08" description="Claude Code plugin v0.3.1">
|
||||
|
||||
**Changed:**
|
||||
@@ -2390,6 +2396,12 @@ Initial release of the Mem0 plugin for Claude Code and Cursor, followed by Codex
|
||||
|
||||
<Tab title="Cursor">
|
||||
|
||||
<Update label="Unreleased" description="Sidekick availability">
|
||||
|
||||
Removes Sidekick and its start/stop hooks. Memory capture, search, and six skills remain available.
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-09-08" description="Cursor plugin v0.3.1">
|
||||
|
||||
**Changed:**
|
||||
@@ -2409,6 +2421,12 @@ Initial release of the Mem0 plugin for Claude Code and Cursor, followed by Codex
|
||||
|
||||
<Tab title="Codex">
|
||||
|
||||
<Update label="Unreleased" description="Sidekick availability">
|
||||
|
||||
Renames shared tracking to use subagent terminology. Native subagent memory support remains available.
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-09-08" description="Codex plugin v0.3.1">
|
||||
|
||||
**Changed:**
|
||||
@@ -2427,6 +2445,12 @@ Initial release of the Mem0 plugin for Claude Code and Cursor, followed by Codex
|
||||
|
||||
<Tab title="Agent Plugins v1">
|
||||
|
||||
<Update label="Unreleased" description="Sidekick availability">
|
||||
|
||||
Sidekick is available only in Claude Code, not in the portable package.
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-09-08" description="Portable Mem0 plugin v0.3.1">
|
||||
|
||||
**Added:**
|
||||
@@ -2543,6 +2567,12 @@ Initial release of the Mem0 plugin for Claude Code and Cursor, followed by Codex
|
||||
|
||||
<Tab title="Antigravity">
|
||||
|
||||
<Update label="Unreleased" description="Sidekick availability">
|
||||
|
||||
Removes Sidekick. Memory capture, search, and six skills remain available.
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-09-08" description="Antigravity plugin v0.3.1">
|
||||
|
||||
**Changed:**
|
||||
@@ -2635,6 +2665,12 @@ Existing memories written by the previous versions are not rewritten. If your me
|
||||
|
||||
<Tab title="Kimi">
|
||||
|
||||
<Update label="Unreleased" description="Sidekick availability">
|
||||
|
||||
Removes Sidekick and its start/stop hooks. Memory capture, recall, and six skills remain available.
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-09-08" description="Kimi Code plugin v0.3.1">
|
||||
|
||||
**Changed:**
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Configurations
|
||||
seo:
|
||||
title: "Embedder Configuration Reference - Mem0"
|
||||
title: "Embedder Configuration Reference"
|
||||
sidebarTitle: "Configurations"
|
||||
description: "Reference for embedder configuration options in Mem0, including provider selection and model settings."
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: AWS Bedrock
|
||||
seo:
|
||||
title: "AWS Bedrock as Embedding Provider - Mem0"
|
||||
title: "AWS Bedrock as Embedding Provider"
|
||||
sidebarTitle: "AWS Bedrock"
|
||||
description: "Configure AWS Bedrock as an embedding provider in Mem0 with IAM credentials and boto3 authentication."
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Azure OpenAI
|
||||
seo:
|
||||
title: "Azure OpenAI as Embedding Provider - Mem0"
|
||||
title: "Azure OpenAI as Embedding Provider"
|
||||
sidebarTitle: "Azure OpenAI"
|
||||
description: "Configure Azure OpenAI as an embedding provider in Mem0 with API key, deployment, and endpoint settings."
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Google AI
|
||||
seo:
|
||||
title: "Google AI as Embedding Provider - Mem0"
|
||||
title: "Google AI as Embedding Provider"
|
||||
sidebarTitle: "Google AI"
|
||||
description: "Configure Google AI as an embedding provider in Mem0 using Gemini models and the GOOGLE_API_KEY variable."
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: LangChain
|
||||
seo:
|
||||
title: "LangChain as Embedding Provider - Mem0"
|
||||
title: "LangChain as Embedding Provider"
|
||||
sidebarTitle: "LangChain"
|
||||
description: "Use LangChain as an embedding provider in Mem0 to access a wide range of models through a unified interface."
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: "LM Studio"
|
||||
seo:
|
||||
title: "LM Studio as Embedding Provider - Mem0"
|
||||
title: "LM Studio as Embedding Provider"
|
||||
sidebarTitle: "LM Studio"
|
||||
description: "Configure LM Studio as an embedding provider in Mem0 for local embedding generation with models like nomic-embed-text."
|
||||
---
|
||||
You can use embedding models from LM Studio to run Mem0 locally.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: "Ollama"
|
||||
seo:
|
||||
title: "Ollama as Embedding Provider - Mem0"
|
||||
title: "Ollama as Embedding Provider"
|
||||
sidebarTitle: "Ollama"
|
||||
description: "Configure Ollama as an embedding provider in Mem0 to generate embeddings locally using open-source models."
|
||||
---
|
||||
You can use embedding models from Ollama to run Mem0 locally.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: OpenAI
|
||||
seo:
|
||||
title: "OpenAI as Embedding Provider - Mem0"
|
||||
title: "OpenAI as Embedding Provider"
|
||||
sidebarTitle: "OpenAI"
|
||||
description: "Configure OpenAI as an embedding provider in Mem0 using models like text-embedding-3-large for vector generation."
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Together
|
||||
seo:
|
||||
title: "Together AI as Embedding Provider - Mem0"
|
||||
title: "Together AI as Embedding Provider"
|
||||
sidebarTitle: "Together"
|
||||
description: "Configure Together AI as an embedding provider in Mem0 with support for 1024-dimensional embedding models."
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Overview
|
||||
seo:
|
||||
title: "Embedding Providers Overview - Mem0"
|
||||
title: "Embedding Providers Overview"
|
||||
sidebarTitle: Overview
|
||||
description: "Overview of all supported embedding model providers in Mem0, including OpenAI, Azure, Ollama, and more."
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Configurations
|
||||
seo:
|
||||
title: "LLM Configuration Reference - Mem0"
|
||||
title: "LLM Configuration Reference"
|
||||
sidebarTitle: "Configurations"
|
||||
description: "Reference for LLM configuration options in Mem0 for Python and TypeScript, including value precedence rules."
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: AWS Bedrock
|
||||
seo:
|
||||
title: "AWS Bedrock as LLM Provider - Mem0"
|
||||
title: "AWS Bedrock as LLM Provider"
|
||||
sidebarTitle: "AWS Bedrock"
|
||||
description: "Configure AWS Bedrock as an LLM provider in Mem0 with IAM authentication and Claude model support."
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Azure OpenAI
|
||||
seo:
|
||||
title: "Azure OpenAI as LLM Provider - Mem0"
|
||||
title: "Azure OpenAI as LLM Provider"
|
||||
sidebarTitle: "Azure OpenAI"
|
||||
description: "Configure Azure OpenAI as an LLM provider in Mem0 with Azure Identity authentication and deployment settings."
|
||||
---
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ title: DeepSeek
|
||||
description: "Configure DeepSeek as an LLM provider in Mem0 with API key setup and optional custom endpoint configuration."
|
||||
---
|
||||
|
||||
To use DeepSeek LLM models, you have to set the `DEEPSEEK_API_KEY` environment variable. You can also optionally set `DEEPSEEK_API_BASE` if you need to use a different API endpoint (defaults to "https://api.deepseek.com").
|
||||
To use DeepSeek LLM models, you have to set the `DEEPSEEK_API_KEY` environment variable. You can also optionally set `DEEPSEEK_API_BASE` if you need to use a different API endpoint (defaults to `https://api.deepseek.com`).
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Google AI
|
||||
seo:
|
||||
title: "Google AI as LLM Provider - Mem0"
|
||||
title: "Google AI as LLM Provider"
|
||||
sidebarTitle: "Google AI"
|
||||
description: "Configure Google Gemini as an LLM provider in Mem0 using the google.genai SDK and GOOGLE_API_KEY variable."
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: LangChain
|
||||
seo:
|
||||
title: "LangChain as LLM Provider - Mem0"
|
||||
title: "LangChain as LLM Provider"
|
||||
sidebarTitle: "LangChain"
|
||||
description: "Use LangChain as an LLM provider in Mem0 to integrate with various chat models through a unified interface."
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: LM Studio
|
||||
seo:
|
||||
title: "LM Studio as LLM Provider - Mem0"
|
||||
title: "LM Studio as LLM Provider"
|
||||
sidebarTitle: "LM Studio"
|
||||
description: "Configure LM Studio as an LLM provider in Mem0 for running local language models via an OpenAI-compatible API."
|
||||
---
|
||||
|
||||
@@ -78,7 +77,7 @@ m.add(messages, user_id="alice123", metadata={"category": "movies"})
|
||||
To use LM Studio, you need to:
|
||||
1. Download and install [LM Studio](https://lmstudio.ai/)
|
||||
2. Start a local server from the "Server" tab
|
||||
3. Set the appropriate `lmstudio_base_url` in your configuration (default is usually http://localhost:1234/v1)
|
||||
3. Set the appropriate `lmstudio_base_url` in your configuration (default is usually `http://localhost:1234/v1`)
|
||||
</Note>
|
||||
|
||||
## Config
|
||||
|
||||
@@ -3,7 +3,7 @@ title: MiniMax
|
||||
description: "Configure MiniMax as an LLM provider in Mem0 with API key setup and optional custom endpoint configuration."
|
||||
---
|
||||
|
||||
To use MiniMax LLM models, you have to set the `MINIMAX_API_KEY` environment variable. You can also optionally set `MINIMAX_API_BASE` if you need to use a different API endpoint (defaults to "https://api.minimax.io/v1").
|
||||
To use MiniMax LLM models, you have to set the `MINIMAX_API_KEY` environment variable. You can also optionally set `MINIMAX_API_BASE` if you need to use a different API endpoint (defaults to `https://api.minimax.io/v1`).
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Ollama
|
||||
seo:
|
||||
title: "Ollama as LLM Provider - Mem0"
|
||||
title: "Ollama as LLM Provider"
|
||||
sidebarTitle: "Ollama"
|
||||
description: "Configure Ollama as an LLM provider in Mem0 for running local language models with tool-calling support."
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: OpenAI
|
||||
seo:
|
||||
title: "OpenAI as LLM Provider - Mem0"
|
||||
title: "OpenAI as LLM Provider"
|
||||
sidebarTitle: "OpenAI"
|
||||
description: "Configure OpenAI as an LLM provider in Mem0 with support for GPT models and Openrouter compatibility."
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Together
|
||||
seo:
|
||||
title: "Together AI as LLM Provider - Mem0"
|
||||
title: "Together AI as LLM Provider"
|
||||
sidebarTitle: "Together"
|
||||
description: "Configure Together AI as an LLM provider in Mem0 with API key setup and optional custom endpoint configuration."
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: xAI
|
||||
seo:
|
||||
title: "xAI Grok as LLM Provider - Mem0"
|
||||
title: "xAI Grok as LLM Provider"
|
||||
sidebarTitle: "xAI"
|
||||
description: "Configure xAI Grok models as an LLM provider in Mem0 with API key setup and usage examples."
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Overview
|
||||
seo:
|
||||
title: "LLM Providers Overview - Mem0"
|
||||
title: "LLM Providers Overview"
|
||||
sidebarTitle: Overview
|
||||
description: "Overview of all supported LLM providers in Mem0, including OpenAI, Anthropic, Groq, Ollama, and more."
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Overview
|
||||
seo:
|
||||
title: "Reranker Providers Overview - Mem0"
|
||||
title: "Reranker Providers Overview"
|
||||
sidebarTitle: "Overview"
|
||||
description: 'Pick the right reranker path to boost Mem0 search relevance.'
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Configurations
|
||||
seo:
|
||||
title: "Vector Store Configuration Reference - Mem0"
|
||||
title: "Vector Store Configuration Reference"
|
||||
sidebarTitle: "Configurations"
|
||||
description: "Reference for vector database configuration options in Mem0, including provider selection and connection settings."
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: LangChain
|
||||
seo:
|
||||
title: "LangChain as Vector Store Provider - Mem0"
|
||||
title: "LangChain as Vector Store Provider"
|
||||
sidebarTitle: "LangChain"
|
||||
description: "Use LangChain as a unified vector store provider in Mem0 to access multiple vector databases through one interface."
|
||||
---
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ Here are the parameters available for configuring Upstash Vector:
|
||||
| `url` | URL for the Upstash Vector index | `None` |
|
||||
| `token` | Token for the Upstash Vector index | `None` |
|
||||
| `client` | An `upstash_vector.Index` instance | `None` |
|
||||
| `collection_name` | The default namespace used | `""` |
|
||||
| `collection_name` | The default namespace used | `"mem0"` |
|
||||
| `enable_embeddings` | Whether to use Upstash embeddings | `False` |
|
||||
|
||||
<Note>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Overview
|
||||
seo:
|
||||
title: "Vector Store Providers Overview - Mem0"
|
||||
title: "Vector Store Providers Overview"
|
||||
sidebarTitle: "Overview"
|
||||
description: "Overview of all supported vector databases in Mem0, including Qdrant, Chroma, PGVector, Pinecone, Oracle, and more."
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Overview
|
||||
seo:
|
||||
title: "Cookbooks and Tutorials - Mem0"
|
||||
title: "Cookbooks and Tutorials"
|
||||
sidebarTitle: "Overview"
|
||||
description: "Browse cookbook examples and tutorials for building AI applications with Mem0, from companion chatbots to AI agents."
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Delete Memory
|
||||
seo:
|
||||
title: "Delete Memory Operation - Mem0"
|
||||
title: "Delete Memory Operation"
|
||||
sidebarTitle: "Delete Memory"
|
||||
description: Remove memories from Mem0 either individually, in bulk, or via filters.
|
||||
icon: "trash"
|
||||
iconType: "solid"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Update Memory
|
||||
seo:
|
||||
title: "Update Memory Operation - Mem0"
|
||||
title: "Update Memory Operation"
|
||||
sidebarTitle: "Update Memory"
|
||||
description: Modify an existing memory by updating its content or metadata.
|
||||
icon: "pen-to-square"
|
||||
iconType: "solid"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Overview
|
||||
seo:
|
||||
title: "Integrations Overview - Mem0"
|
||||
title: "Integrations Overview"
|
||||
sidebarTitle: "Overview"
|
||||
description: "Overview of Mem0 integrations with popular AI frameworks and tools for persistent memory and context management."
|
||||
---
|
||||
|
||||
@@ -17,6 +16,8 @@ Mem0 seamlessly integrates with popular AI frameworks and tools to enhance your
|
||||
**Universal Integration**: Use <Link href="/platform/mem0-mcp">Mem0 MCP</Link> for a standardized protocol that works with ANY AI client.
|
||||
</Callout>
|
||||
|
||||
Sidekick is available only in the [Claude Code plugin](/integrations/claude-code#sidekick-agent). Other plugins, including the portable package, provide memory without Sidekick.
|
||||
|
||||
Here are the available integrations for Mem0:
|
||||
|
||||
## Integrations
|
||||
|
||||
@@ -7,6 +7,8 @@ Add persistent memory to [**Google Antigravity**](https://antigravity.google) (`
|
||||
|
||||
<Info>Current plugin version: `0.3.1`.</Info>
|
||||
|
||||
Sidekick is available only in the [Claude Code plugin](/integrations/claude-code#sidekick-agent).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. A Mem0 API key (starts with `m0-`):
|
||||
@@ -48,11 +50,10 @@ This installs the generated Antigravity bundle with the Mem0 server, lifecycle h
|
||||
| Local MCP Server (`search_memories`) | Yes |
|
||||
| Lifecycle Hooks | Yes |
|
||||
| 6 Memory Skills | Yes |
|
||||
| Sidekick Agent | Yes |
|
||||
|
||||
## Memory tools and skills
|
||||
|
||||
The full plugin exposes a focused `search_memories` tool and captures completed work through lifecycle hooks. Six skills provide search, status, remember, forget, pause, and resume workflows. A native sidekick can handle a bounded task in the workspace assigned by Antigravity; it does not claim Claude Code worktree isolation.
|
||||
The full plugin exposes a focused `search_memories` tool and captures completed work through lifecycle hooks. Six skills provide search, status, remember, forget, pause, and resume workflows.
|
||||
|
||||
If you need the full set of direct CRUD tools, connect the [hosted Mem0 MCP server](/platform/mem0-mcp) separately instead of installing both configurations under the same server name.
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: AWS Bedrock
|
||||
seo:
|
||||
title: "AWS Bedrock Integration with Mem0"
|
||||
title: "AWS Bedrock Integration"
|
||||
sidebarTitle: "AWS Bedrock"
|
||||
description: "Use Mem0 with AWS Bedrock and OpenSearch Service for cloud-native persistent semantic memory storage."
|
||||
---
|
||||
|
||||
|
||||
@@ -64,6 +64,8 @@ After the automatic first-prompt search, Claude can also call `search_memories`
|
||||
|
||||
### Sidekick agent
|
||||
|
||||
Sidekick is available only in Claude Code.
|
||||
|
||||
`mem0:sidekick` is a Sonnet coding agent that runs in a separate Git worktree. Use it to offload investigation, implementation, testing, or review without burning main-session context.
|
||||
|
||||
```text
|
||||
|
||||
@@ -3,10 +3,12 @@ title: Codex
|
||||
description: "Add persistent memory to OpenAI Codex with automatic capture, automatic recall, a search tool, and six memory skills."
|
||||
---
|
||||
|
||||
Add persistent memory to [**OpenAI Codex**](https://openai.com/index/codex/) with the Mem0 plugin. Codex forgets everything between tasks. This plugin fixes that by connecting to Mem0's cloud memory layer via MCP, automatically capturing learnings at key lifecycle points, and retrieving relevant context on the first prompt of a session. Codex can use the search tool for recall later in the session.
|
||||
Add persistent memory to [**OpenAI Codex**](https://openai.com/codex/) with the Mem0 plugin. Codex forgets everything between tasks. This plugin fixes that by connecting to Mem0's cloud memory layer via MCP, automatically capturing learnings at key lifecycle points, and retrieving relevant context on the first prompt of a session. Codex can use the search tool for recall later in the session.
|
||||
|
||||
<Info>Current plugin version: `0.3.1`.</Info>
|
||||
|
||||
Sidekick is available only in the [Claude Code plugin](/integrations/claude-code#sidekick-agent).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before setting up Mem0 with Codex, ensure you have:
|
||||
|
||||
@@ -7,6 +7,8 @@ Add persistent memory to [**Cursor**](https://cursor.com) with the Mem0 plugin.
|
||||
|
||||
<Info>Current plugin version: `0.3.1`.</Info>
|
||||
|
||||
Sidekick is available only in the [Claude Code plugin](/integrations/claude-code#sidekick-agent).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before setting up Mem0 with Cursor, ensure you have:
|
||||
@@ -49,7 +51,7 @@ cursor-agent plugin marketplace add https://github.com/mem0ai/mem0
|
||||
|
||||
Open Cursor's **Customize** page or run `/plugins`, select the **Mem0 Plugins** marketplace, and install **Mem0** at user or project scope. Enter your Mem0 API key when Cursor asks for the plugin configuration, then restart Cursor.
|
||||
|
||||
The full plugin includes automatic capture, the `search_memories` recall tool, lifecycle hooks, a sidekick agent, and six memory skills.
|
||||
The full plugin includes automatic capture, the `search_memories` recall tool, lifecycle hooks, and six memory skills.
|
||||
|
||||
### Option B: One-Click Deeplink (MCP Only)
|
||||
|
||||
@@ -98,7 +100,6 @@ Add the following to your `.cursor/mcp.json`:
|
||||
| Recall | `search_memories` | MCP search tools |
|
||||
| Lifecycle hooks | Yes | No |
|
||||
| Memory skills | 6 | No |
|
||||
| Sidekick agent | Yes | No |
|
||||
|
||||
## MCP-only tools
|
||||
|
||||
@@ -125,15 +126,14 @@ The full plugin translates Cursor's native events into the shared Mem0 memory li
|
||||
| `sessionStart` | Initializes the project session and recovers pending capture |
|
||||
| `beforeSubmitPrompt` | Observes the submitted prompt; recall stays tool-driven because this event cannot inject context |
|
||||
| `postToolUse` / `postToolUseFailure` | Records useful tool results and failures |
|
||||
| `subagentStart` / `subagentStop` | Records the Mem0 Sidekick lifecycle and completed result |
|
||||
| `afterAgentResponse` / `stop` | Captures the completed exchange |
|
||||
| `preCompact` / `sessionEnd` | Flushes pending capture in the background |
|
||||
|
||||
<Note>
|
||||
Cursor's `beforeSubmitPrompt` hook can allow or block a prompt, but it cannot add context to that prompt. The plugin therefore gives the agent `search_memories` and a memory-aware sidekick for recall instead of claiming automatic per-prompt injection.
|
||||
Cursor's `beforeSubmitPrompt` hook cannot add context to a prompt. Use `search_memories` to recall memories.
|
||||
</Note>
|
||||
|
||||
Cursor's `subagentStart` hook also cannot inject parent context. The bundled Sidekick searches Mem0 itself, while the lifecycle hooks correlate its start and completion for later capture.
|
||||
The plugin does not register subagent start or stop hooks.
|
||||
|
||||
## Example Workflow
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ Add persistent memory to the [**DeepSeek Harness**](https://github.com/deepseek-
|
||||
|
||||
<Info>Current package version: `0.3.0`.</Info>
|
||||
|
||||
Sidekick is available only in the [Claude Code plugin](/integrations/claude-code#sidekick-agent).
|
||||
|
||||
## Overview
|
||||
|
||||
The plugin provides automatic memory plus two agent-callable tools:
|
||||
|
||||
@@ -23,7 +23,7 @@ npm install -g flowise
|
||||
npx flowise start
|
||||
```
|
||||
|
||||
2. Access to the Flowise UI at http://localhost:3000
|
||||
2. Access to the Flowise UI at `http://localhost:3000`
|
||||
3. Basic familiarity with [Flowise's LLM orchestration](https://flowiseai.com/#features) concepts
|
||||
|
||||
## Setup and Configuration
|
||||
|
||||
@@ -1,39 +1,35 @@
|
||||
---
|
||||
title: Hermes Agent
|
||||
description: "Add long-term memory to Hermes agents with Mem0, on managed Mem0 Cloud or fully self-hosted (OSS), with automatic background sync and zero-latency prefetch."
|
||||
description: "Add long-term memory to Hermes agents with Mem0: managed Platform, a self-hosted server, or fully local OSS — with current-turn recall and background fact extraction."
|
||||
---
|
||||
|
||||
Add long-term memory to [Hermes Agent](https://github.com/NousResearch/hermes-agent), a self-improving AI agent CLI by Nous Research. Hermes has a pluggable memory system, and Mem0 is one of the supported providers. Once enabled, Mem0 learns facts from your conversations and surfaces relevant ones before each turn, without slowing down the chat.
|
||||
Add long-term memory to [Hermes Agent](https://github.com/NousResearch/hermes-agent), a self-improving AI agent CLI by Nous Research. Hermes has a pluggable memory system, and Mem0 is one of the supported providers. Once enabled, Mem0 learns facts from your conversations and surfaces relevant ones for the current question, without slowing down the chat.
|
||||
|
||||
You can run Mem0 in two ways:
|
||||
You can run Mem0 in three ways:
|
||||
|
||||
- **Platform mode** (default): managed Mem0 Cloud. Add your API key and you are ready.
|
||||
- **OSS mode**: fully self-hosted with your own LLM, embedder, and vector store. No data leaves your machine.
|
||||
- **Self-hosted server mode**: point the plugin at a Mem0 server you run yourself (the Docker-shipped server). The plugin only talks HTTP to your server.
|
||||
- **OSS mode**: run Mem0 in-process with your own LLM, embedder, and vector store. No Mem0 server required.
|
||||
|
||||
## How It Works
|
||||
|
||||
Hermes runs a built-in memory system (file-based `MEMORY.md` and `USER.md`) alongside one external provider. When Mem0 is active, it works additively with the built-in system at three points in every conversation turn.
|
||||
Hermes runs a built-in memory system (file-based `MEMORY.md` and `USER.md`) alongside one external provider. When Mem0 is active, it works additively with the built-in system at two points in every conversation turn.
|
||||
|
||||
### 1. Before the agent responds (prefetch)
|
||||
### 1. Current-turn recall (bounded wait)
|
||||
|
||||
When you send a message, Hermes checks for cached Mem0 search results from the previous turn. If they exist, those memories are injected into the system prompt so the model can see them. This is zero-latency, with no waiting on an API call.
|
||||
When you send a message, Hermes searches your stored memories for the current question and waits up to 3 seconds for results. If they arrive in time, they are injected into the system prompt so the model can see them. If the backend is slower, Hermes skips the injection and the model can still call `mem0_search` itself — so a slow backend never blocks a turn.
|
||||
|
||||
### 2. After the agent responds (sync)
|
||||
### 2. Background fact extraction (sync)
|
||||
|
||||
Once the model finishes, Hermes sends the `(user message, assistant response)` pair to Mem0 in a background thread. Mem0 extracts facts automatically (for example, "user prefers Python" or "user works at Acme Corp"), so you never have to tell it what to remember. Each write is tagged with the gateway channel it came from.
|
||||
|
||||
### 3. Background prefetch for the next turn
|
||||
|
||||
At the same time, Hermes runs a background search to pre-load relevant memories for your next message. By the time you type, the results are already cached.
|
||||
|
||||
## Agent Tools
|
||||
|
||||
When Mem0 is active, the model gets five tools it can call during a conversation:
|
||||
When Mem0 is active, the model gets four tools it can call during a conversation:
|
||||
|
||||
| Tool | Description | Parameters |
|
||||
|------|-------------|------------|
|
||||
| `mem0_list` | List all stored memories, for a full overview | `page`, `page_size` (default 100, max 200) |
|
||||
| `mem0_search` | Semantic search by meaning, ranked by relevance | `query` (required), `top_k` (default 10, max 50), `rerank` (default `true`, Platform mode only) |
|
||||
| `mem0_search` | Semantic search by meaning, ranked by relevance | `query` (required), `top_k` (default 10, max 50), `rerank` (default `false`, Platform mode only) |
|
||||
| `mem0_add` | Store a fact verbatim, with no LLM extraction | `content` (required) |
|
||||
| `mem0_update` | Update a memory's text by ID | `memory_id`, `text` (both required) |
|
||||
| `mem0_delete` | Delete a memory by ID | `memory_id` (required) |
|
||||
@@ -79,6 +75,36 @@ memory:
|
||||
|
||||
That's it. Mem0 runs automatically from here.
|
||||
|
||||
## Self-Hosted Server Setup
|
||||
|
||||
Run the [Mem0 server](https://github.com/mem0ai/mem0/tree/main/server) (FastAPI + pgvector) from its Docker image and point the plugin at it. Unlike OSS mode, the plugin just talks HTTP to your server.
|
||||
|
||||
### Interactive
|
||||
|
||||
```bash
|
||||
hermes memory setup
|
||||
# Select "mem0", then "Self-hosted server", and enter the server URL
|
||||
```
|
||||
|
||||
### With flags
|
||||
|
||||
```bash
|
||||
hermes memory setup mem0 --mode selfhosted \
|
||||
--host http://localhost:8888 \
|
||||
--api-key your-admin-api-key
|
||||
```
|
||||
|
||||
### With environment variables
|
||||
|
||||
```bash
|
||||
echo "MEM0_HOST=http://localhost:8888" >> ~/.hermes/.env
|
||||
echo "MEM0_API_KEY=your-admin-api-key" >> ~/.hermes/.env
|
||||
```
|
||||
|
||||
Then start a fresh Hermes session and call `mem0_search` — it connects to your server. The plugin authenticates with `X-API-Key` and uses the server's `/search` and `/memories` routes. The API key is optional only for servers running with `AUTH_DISABLED`.
|
||||
|
||||
<Note>Setting `host` routes to the self-hosted server automatically. Don't combine it with `mode: oss` — OSS takes precedence and ignores `host`.</Note>
|
||||
|
||||
## OSS (Self-Hosted) Setup
|
||||
|
||||
OSS mode runs Mem0 entirely on your own infrastructure: your LLM, your embedder, and your vector store. No data is sent to Mem0 Cloud, and no Mem0 API key is required.
|
||||
@@ -111,15 +137,20 @@ hermes memory setup mem0 --mode oss \
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--mode` | `platform` or `oss` |
|
||||
| `--mode` | `platform`, `selfhosted`, or `oss` |
|
||||
| `--api-key` | Platform API key, or the admin key of a self-hosted server |
|
||||
| `--host` | Self-hosted server URL (with `--mode selfhosted`) |
|
||||
| `--oss-llm` | LLM provider (`openai` or `ollama`, default `openai`) |
|
||||
| `--oss-llm-key` | LLM API key (for `openai`) |
|
||||
| `--oss-llm-model` | Override the LLM model |
|
||||
| `--oss-llm-url` | LLM base URL (for `ollama` or a custom endpoint) |
|
||||
| `--oss-embedder` | Embedder provider (default `openai`) |
|
||||
| `--oss-embedder-key` | Embedder API key |
|
||||
| `--oss-embedder-model` | Override the embedder model |
|
||||
| `--oss-embedder-url` | Embedder base URL (for `ollama` or a custom endpoint) |
|
||||
| `--oss-vector` | Vector store (`qdrant` or `pgvector`, default `qdrant`) |
|
||||
| `--oss-vector-path` | Local Qdrant storage path |
|
||||
| `--oss-vector-url` | Qdrant server URL |
|
||||
| `--oss-vector-host`, `--oss-vector-port` | PGVector or remote Qdrant host and port |
|
||||
| `--oss-vector-user`, `--oss-vector-password`, `--oss-vector-dbname` | PGVector connection details |
|
||||
| `--user-id` | Canonical user identifier |
|
||||
@@ -127,7 +158,7 @@ hermes memory setup mem0 --mode oss \
|
||||
|
||||
## Switching Modes
|
||||
|
||||
You can move between Platform and OSS at any time. Run the setup command again, or edit `~/.hermes/mem0.json` directly.
|
||||
You can move between the three modes at any time. Run the setup command again, or edit `~/.hermes/mem0.json` directly.
|
||||
|
||||
```bash
|
||||
# Platform to OSS
|
||||
@@ -136,6 +167,9 @@ hermes memory setup mem0 --mode oss --oss-llm-key sk-...
|
||||
# OSS to Platform
|
||||
hermes memory setup mem0 --mode platform --api-key sk-...
|
||||
|
||||
# Platform to a self-hosted server
|
||||
hermes memory setup mem0 --mode selfhosted --host http://localhost:8888
|
||||
|
||||
# Preview without writing anything
|
||||
hermes memory setup mem0 --mode oss --oss-llm-key sk-... --dry-run
|
||||
```
|
||||
@@ -146,7 +180,7 @@ A self-hosted `~/.hermes/mem0.json` looks like this:
|
||||
{
|
||||
"mode": "oss",
|
||||
"oss": {
|
||||
"llm": {"provider": "openai", "config": {"model": "gpt-5-mini"}},
|
||||
"llm": {"provider": "openai", "config": {"model": "gpt-5-mini", "is_reasoning_model": true}},
|
||||
"embedder": {"provider": "openai", "config": {"model": "text-embedding-3-small"}},
|
||||
"vector_store": {"provider": "qdrant", "config": {"path": "~/.hermes/mem0_qdrant"}}
|
||||
}
|
||||
@@ -159,11 +193,12 @@ Behavioral settings live in `~/.hermes/mem0.json` and are written for you by `he
|
||||
|
||||
| Key | Default | Description |
|
||||
|-----|---------|-------------|
|
||||
| `mode` | `platform` | `platform` (Mem0 Cloud) or `oss` (self-hosted) |
|
||||
| `api_key` | none | Mem0 Platform API key, required in Platform mode. Stored in `.env` as `MEM0_API_KEY` |
|
||||
| `mode` | `platform` | `platform` (Mem0 Cloud) or `oss` (self-managed, in-process). Self-hosted server routing is set via `host` |
|
||||
| `host` | none | Self-hosted Mem0 server URL. When set, the plugin talks HTTP to your server instead of the cloud |
|
||||
| `api_key` | none | Mem0 Platform API key, or the admin key of a self-hosted server. Stored in `.env` as `MEM0_API_KEY` |
|
||||
| `user_id` | `hermes-user` | Identifier that scopes memories. See cross-channel behavior below |
|
||||
| `agent_id` | `hermes` | Agent identifier attached to writes |
|
||||
| `rerank` | `true` | Rerank search results for relevance (Platform mode only) |
|
||||
| `rerank` | `false` | Rerank search results for relevance (Platform mode only) |
|
||||
|
||||
### Cross-channel memories
|
||||
|
||||
@@ -174,12 +209,11 @@ Hermes can run from the CLI and from gateways like Telegram, Slack, and Discord.
|
||||
|
||||
Either way, every write is tagged with `metadata.channel` (for example `telegram` or `cli`), so per-channel views are still possible at query time.
|
||||
|
||||
|
||||
## Reliability
|
||||
|
||||
- **Circuit breaker**: if Mem0 fails five times in a row, Hermes pauses calls for two minutes, then retries. The agent keeps working without memory during that window. Expected client errors, like a 404 on a missing memory id, do not count toward tripping the breaker.
|
||||
- **Non-blocking**: every Mem0 call runs in a background daemon thread, so a slow or failed call never blocks your conversation.
|
||||
- **Thread-safe**: the client uses lazy initialization with locking, and the background sync and prefetch threads are guarded so concurrent gateway messages cannot produce duplicate memories.
|
||||
- **Non-blocking**: fact extraction runs in a background daemon thread, and current-turn recall waits at most 3 seconds, so a slow or failed call never blocks your conversation.
|
||||
- **Thread-safe**: the client uses lazy initialization with locking, and the background sync and recall threads are guarded so concurrent gateway messages cannot produce duplicate memories.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -188,6 +222,7 @@ Either way, every write is tagged with `metadata.channel` (for example `telegram
|
||||
The circuit breaker tripped after five consecutive failures and resets after two minutes.
|
||||
|
||||
- **Platform mode**: check your API key and internet connection.
|
||||
- **Self-hosted server mode**: check that the server is running and reachable at the configured `host` URL.
|
||||
- **OSS mode**: make sure your vector store (Qdrant or PGVector) is running and reachable.
|
||||
|
||||
### OSS: vector store connection refused
|
||||
@@ -217,8 +252,8 @@ curl http://localhost:11434/api/tags
|
||||
|
||||
## Key Features
|
||||
|
||||
1. **Two ways to run**: managed Platform or fully self-hosted OSS, switchable at any time.
|
||||
2. **Zero-latency recall**: memories are prefetched in the background and cached before you type.
|
||||
1. **Three ways to run**: managed Platform, a self-hosted server, or fully local OSS, switchable at any time.
|
||||
2. **Current-turn recall**: memories for the current question are injected within a 3-second window, with `mem0_search` as the model's own backstop.
|
||||
3. **Automatic extraction**: Mem0 extracts and deduplicates facts from each exchange for you.
|
||||
4. **Non-blocking and fault tolerant**: background threads plus a circuit breaker keep the agent responsive even when Mem0 is unreachable.
|
||||
5. **Additive memory**: works alongside Hermes' built-in file memory (`MEMORY.md`, `USER.md`).
|
||||
|
||||
@@ -7,6 +7,8 @@ Kimi Code forgets project decisions between sessions. The Mem0 plugin captures c
|
||||
|
||||
<Info>Current plugin version: `0.3.1`.</Info>
|
||||
|
||||
Sidekick is available only in the [Claude Code plugin](/integrations/claude-code#sidekick-agent).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. A Mem0 Platform account and API key:
|
||||
@@ -33,7 +35,7 @@ Inside Kimi Code, install the native plugin bundle and reload the session:
|
||||
/reload
|
||||
```
|
||||
|
||||
Run `/plugins info mem0` to confirm that the plugin, MCP server, skills, hooks, and sidekick agent loaded.
|
||||
Run `/plugins info mem0` to confirm that the plugin, MCP server, skills, and hooks loaded.
|
||||
|
||||
## What you get
|
||||
|
||||
@@ -42,7 +44,6 @@ Run `/plugins info mem0` to confirm that the plugin, MCP server, skills, hooks,
|
||||
- **Explicit search:** Kimi can call `search_memories` when it needs a more specific answer.
|
||||
- **Six memory skills:** Search, status, remember, forget, pause, and resume use the same memory behavior as the other Mem0 coding-agent plugins.
|
||||
- **Project scoping:** Memories stay attached to the repository, with separate personal and shared project lanes.
|
||||
- **Kimi sidekick:** A focused subagent can investigate or implement a bounded task in a separate context. Filesystem isolation depends on the environment Kimi provides.
|
||||
|
||||
Credentials are redacted before memory capture. If Mem0 is unavailable, hooks fail open so Kimi can continue its normal work.
|
||||
|
||||
@@ -57,7 +58,8 @@ Kimi's native lifecycle events are translated into the shared Mem0 memory lifecy
|
||||
| `PostToolUse` / `PostToolUseFailure` | Records useful tool results and failures |
|
||||
| `Stop` | Captures the completed exchange |
|
||||
| `PreCompact` / `SessionEnd` | Flushes pending capture in the background |
|
||||
| `SubagentStart` / `SubagentStop` | Passes parent context to the Mem0 sidekick and records its lifecycle |
|
||||
|
||||
The plugin does not register subagent start or stop hooks or supply parent memories to child agents.
|
||||
|
||||
## Verify the plugin
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Langchain
|
||||
seo:
|
||||
title: "LangChain Integration with Mem0"
|
||||
title: "LangChain Integration"
|
||||
sidebarTitle: "Langchain"
|
||||
description: "Build personalized AI agents using LangChain for conversation flow and Mem0 for long-term memory retention."
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: n8n
|
||||
seo:
|
||||
title: "n8n Integration with Mem0"
|
||||
title: "n8n Integration"
|
||||
sidebarTitle: "n8n"
|
||||
description: "Add long-term memory to n8n workflows and AI Agents with the Mem0 community node, no code required."
|
||||
---
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ Add long-term memory to [OpenClaw](https://github.com/openclaw/openclaw) agents
|
||||
|
||||
<Info>Current package version: `1.1.0`.</Info>
|
||||
|
||||
Sidekick is available only in the [Claude Code plugin](/integrations/claude-code#sidekick-agent). OpenClaw subagents can recall parent memories. Mem0 does not capture their sessions.
|
||||
|
||||
## Overview
|
||||
|
||||
<Frame>
|
||||
|
||||
@@ -7,6 +7,8 @@ Add persistent memory to [**OpenCode**](https://opencode.ai) with the Mem0 plugi
|
||||
|
||||
<Info>Current package version: `0.3.0`.</Info>
|
||||
|
||||
Sidekick is available only in the [Claude Code plugin](/integrations/claude-code#sidekick-agent).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. A Mem0 API key (starts with `m0-`):
|
||||
|
||||
@@ -7,6 +7,8 @@ Add persistent memory to [**Pi Agent**](https://pi.dev) with `@mem0/pi-agent-plu
|
||||
|
||||
<Info>Current package version: `0.3.0`.</Info>
|
||||
|
||||
Sidekick is available only in the [Claude Code plugin](/integrations/claude-code#sidekick-agent).
|
||||
|
||||
## Overview
|
||||
|
||||
The plugin provides:
|
||||
|
||||
+4
-4
@@ -269,11 +269,11 @@ If the user is on a pre-current major (Python < 2, TS < 3, or a Platform call st
|
||||
### AI Coding Tools
|
||||
- [Claude Code](https://docs.mem0.ai/integrations/claude-code) [Platform]: Use when wiring memory into Claude Code.
|
||||
- [Claude.ai](https://docs.mem0.ai/integrations/claude-ai) [Platform]: Use when connecting Mem0 to Claude.ai (the hosted web app) via a custom remote MCP connector, or when Claude's native memory seems to be crowding out mem0 tool calls.
|
||||
- [Cursor](https://docs.mem0.ai/integrations/cursor) [Platform]: Use when adding lifecycle capture, explicit memory recall, six skills, and a sidekick to Cursor.
|
||||
- [Cursor](https://docs.mem0.ai/integrations/cursor) [Platform]: Use when adding lifecycle capture, explicit memory recall, and six memory skills to Cursor.
|
||||
- [Codex](https://docs.mem0.ai/integrations/codex) [Platform]: Use when adding automatic capture and recall, six memory skills, and a search tool to OpenAI Codex.
|
||||
- [Kimi Code](https://docs.mem0.ai/integrations/kimi) [Platform]: Use when adding persistent project memory to Kimi Code through its native plugin lifecycle.
|
||||
- [OpenCode](https://docs.mem0.ai/integrations/opencode) [Platform]: Use when adding ten native SDK memory tools, automatic context, seven skills, and project scoping to OpenCode.
|
||||
- [Antigravity](https://docs.mem0.ai/integrations/antigravity) [Platform]: Use when adding lifecycle capture, explicit recall, six memory skills, and a native sidekick to Google Antigravity.
|
||||
- [Antigravity](https://docs.mem0.ai/integrations/antigravity) [Platform]: Use when adding lifecycle capture, explicit recall, and six memory skills to Google Antigravity.
|
||||
|
||||
### Voice & Real-time
|
||||
- [LiveKit](https://docs.mem0.ai/integrations/livekit) [Platform]: Use when building real-time voice/video with memory.
|
||||
@@ -418,13 +418,13 @@ Each subdirectory is a Claude Code Skill (`SKILL.md` + supporting assets). Load
|
||||
|
||||
Source: https://github.com/mem0ai/mem0/tree/main/integrations/claude-code-plugin
|
||||
|
||||
The self-contained Claude Code plugin lives in `integrations/claude-code-plugin/` (v0.3.1, installs as `mem0@mem0-plugins`). It captures evidence locally through lifecycle hooks, extracts memories in a detached background worker, and exposes a single local MCP tool, `search_memories`, plus six `/mem0:*` skills and the unchanged `mem0:sidekick` agent. Pure-stdlib Python, nothing to install.
|
||||
The self-contained Claude Code plugin lives in `integrations/claude-code-plugin/` (v0.3.1, installs as `mem0@mem0-plugins`). It captures evidence locally through lifecycle hooks, extracts memories in a detached background worker, and exposes a single local MCP tool, `search_memories`, plus six `/mem0:*` skills and `mem0:sidekick`, available only in Claude Code. Pure-stdlib Python, nothing to install.
|
||||
|
||||
### Coding-Agent Plugin Sources
|
||||
|
||||
Source: https://github.com/mem0ai/mem0/tree/main/integrations/agent-plugin-core
|
||||
|
||||
The `integrations/agent-plugin-core/` directory is the single source for shared Python and TypeScript memory behavior. Native plugins live in their own sibling directories, while `integrations/mem0-agent-plugin/` is the single portable Agent Plugins v1 package. The portable package provides search and skills but no automatic capture; its remember skill cannot save a new memory on its own. Shared Python search accepts optional `run_id` for session-specific recall in every scope. TypeScript integrations reuse the core's lifecycle, formatting, identity, scoping, and telemetry utilities while keeping their public packages and native host APIs unchanged.
|
||||
The `integrations/agent-plugin-core/` directory is the single source for shared Python and TypeScript memory behavior. Native plugins live in their own sibling directories, while `integrations/mem0-agent-plugin/` is the single portable Agent Plugins v1 package. Sidekick is available only in Claude Code. The portable package provides search and skills but no automatic capture; its remember skill cannot save a new memory on its own. Shared Python search accepts optional `run_id` for session-specific recall in every scope. TypeScript integrations reuse the core's lifecycle, formatting, identity, scoping, and telemetry utilities while keeping their public packages and native host APIs unchanged.
|
||||
|
||||
Editor-specific setup docs (already listed above under `## Integrations > AI Coding Tools`):
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Custom Instructions
|
||||
seo:
|
||||
title: "Open Source Custom Instructions - Mem0"
|
||||
title: "Open Source Custom Instructions"
|
||||
sidebarTitle: "Custom Instructions"
|
||||
description: Tailor fact extraction so Mem0 stores only the details you care about.
|
||||
icon: "wand-magic-sparkles"
|
||||
---
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Multimodal Support
|
||||
seo:
|
||||
title: "Open Source Multimodal Support - Mem0"
|
||||
title: "Open Source Multimodal Support"
|
||||
sidebarTitle: "Multimodal Support"
|
||||
description: Capture and recall memories from both text and images.
|
||||
icon: "image"
|
||||
---
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: "Overview"
|
||||
seo:
|
||||
title: "Open Source Features Overview - Mem0"
|
||||
title: "Open Source Features Overview"
|
||||
sidebarTitle: "Overview"
|
||||
description: "Self-hosting features that extend Mem0 beyond basic memory storage"
|
||||
icon: "list"
|
||||
---
|
||||
|
||||
@@ -56,7 +56,7 @@ The Mem0 REST API server exposes every OSS memory operation over HTTP. Run it al
|
||||
make bootstrap # starts Compose, creates an admin, issues the first API key
|
||||
```
|
||||
|
||||
Or to start the stack only and finish setup via the browser wizard at http://localhost:3000:
|
||||
Or to start the stack only and finish setup via the browser wizard at `http://localhost:3000`:
|
||||
|
||||
```bash
|
||||
cd server
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: "Overview"
|
||||
seo:
|
||||
title: "Mem0 Open Source Overview"
|
||||
title: "Open Source Overview"
|
||||
sidebarTitle: "Overview"
|
||||
description: "Self-host Mem0 with full control over your infrastructure and data"
|
||||
icon: "house"
|
||||
---
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: CLI
|
||||
seo:
|
||||
title: "Mem0 CLI for Terminal Memory Management"
|
||||
title: "CLI for Terminal Memory Management"
|
||||
sidebarTitle: "CLI"
|
||||
description: "Manage memories from your terminal, for both humans and AI agents."
|
||||
icon: "terminal"
|
||||
iconType: "solid"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Custom Instructions
|
||||
seo:
|
||||
title: "Platform Custom Instructions - Mem0"
|
||||
title: "Platform Custom Instructions"
|
||||
sidebarTitle: "Custom Instructions"
|
||||
description: 'Control how Mem0 extracts and stores memories using natural language guidelines'
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Multimodal Support
|
||||
seo:
|
||||
title: "Platform Multimodal Support - Mem0"
|
||||
title: "Platform Multimodal Support"
|
||||
sidebarTitle: "Multimodal Support"
|
||||
description: Integrate images and documents into your interactions with Mem0
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: "Overview"
|
||||
seo:
|
||||
title: "Mem0 Platform Overview"
|
||||
title: "Platform Overview"
|
||||
sidebarTitle: "Overview"
|
||||
description: "Managed memory layer for AI agents, production-ready in minutes"
|
||||
icon: "cloud"
|
||||
---
|
||||
|
||||
@@ -23,7 +23,7 @@ integrations/
|
||||
|
||||
Each native directory owns only its manifest, native hooks or adapter, tests, and `plugin-build.json`. Its `core/` and `skills/` directories are generated from this module. They are committed because clients install a self-contained plugin directory and the Agent Plugins specification forbids package files from resolving outside the plugin root.
|
||||
|
||||
Claude Code remains the behavioral source of truth. Its sidekick stays at `claude-code-plugin/agents/sidekick.md` and is not generated or copied to hosts without a compatible native subagent interface.
|
||||
Sidekick belongs only to Claude Code. Its agent definition is in `claude-code-plugin/agents/sidekick.md`; its hooks are in `claude-code-plugin/adapters/claude/hook.py`. Other plugins must not register Sidekick. The shared core handles memory and native subagent tracking for Claude Code and Codex.
|
||||
|
||||
TypeScript integrations (`openclaw`, `opencode-plugin`, `pi-agent-plugin`, and `deepseek-plugin`) import `typescript/src/` at build time. Their package builders include the shared implementation in their normal output; they do not carry checked-in copies.
|
||||
|
||||
@@ -106,19 +106,21 @@ For another native Python host:
|
||||
|
||||
Keep capture, recall, memory scoping, redaction, skill text, and telemetry in this shared module. Host directories should contain only behavior required by their native SDK.
|
||||
|
||||
For a TypeScript host, import the shared lifecycle modules directly and keep only native SDK registration in the integration. Do not advertise capture, compaction, or sidekick behavior unless the host exposes the necessary lifecycle seam.
|
||||
For a TypeScript host, import the shared lifecycle modules directly and keep only native SDK registration in the integration. Do not advertise capture or compaction behavior unless the host exposes the necessary lifecycle seam. Sidekick is limited to Claude Code.
|
||||
|
||||
## Host capture capabilities
|
||||
|
||||
| Host | Conversation capture | Tool outcomes | Subagent context and correlation |
|
||||
| --- | --- | --- | --- |
|
||||
| Claude Code | Incremental active transcript branch | Native success/failure hooks | Parent context; native agent ID |
|
||||
| Cursor | Prompt and response hooks; duplicate responses suppressed | Native success/failure hooks | Sidekick searches itself; no parent-injection claim |
|
||||
| Cursor | Prompt and response hooks; duplicate responses suppressed | Native success/failure hooks | No plugin subagent hooks or agent declaration |
|
||||
| Codex | Native prompt and final-response fields | Structured failure indicators when present; otherwise unknown | Parent context; native agent ID |
|
||||
| Kimi | Prompt hooks and completed v2 wire output | Native success/failure hooks | Parent context; without an ID, a stop matches only one unambiguous active run |
|
||||
| Antigravity | Incremental completed transcript messages, including later prompts | Native tool errors | Sidekick searches itself; no worktree-isolation claim |
|
||||
| Kimi | Prompt hooks and completed v2 wire output | Native success/failure hooks | No plugin subagent hooks or agent declaration |
|
||||
| Antigravity | Incremental completed transcript messages, including later prompts | Native tool errors | No plugin subagent hooks or agent declaration |
|
||||
| Portable v1 | Explicit memory skills | No native lifecycle hooks | No native subagent declaration |
|
||||
|
||||
Python status uses `subagent_runs` and `last_subagent`. Legacy SQLite names and event handling keep existing records and running workers compatible.
|
||||
|
||||
An uncorrelated subagent completion is kept as its own record; the plugin never guesses which overlapping run completed. Codex's documented hook fields already match the shared input contract, so no speculative field aliases or unsupported failure event are registered.
|
||||
|
||||
Offline conformance exercises the adapters and MCP servers with native-shaped payloads and builds each distributable package. It does not establish that every installed editor or Harness version loads the plugin correctly; those checks require smoke tests in the actual hosts.
|
||||
|
||||
@@ -11,10 +11,10 @@ import telemetry
|
||||
from memory_core import (
|
||||
EvidenceStore,
|
||||
api_key,
|
||||
configure_harness,
|
||||
data_dir,
|
||||
doctor,
|
||||
forget_remote_repo,
|
||||
configure_harness,
|
||||
resolve_repo,
|
||||
user_id,
|
||||
)
|
||||
@@ -32,7 +32,7 @@ def _print_status(value: dict) -> None:
|
||||
)
|
||||
print(
|
||||
f"Used in this repository: {value['retrievals']} memories returned, "
|
||||
f"{value['sidekick_runs']} sidekick runs"
|
||||
f"{value['subagent_runs']} subagent runs"
|
||||
)
|
||||
if last:
|
||||
item_label = ""
|
||||
@@ -48,13 +48,13 @@ def _print_status(value: dict) -> None:
|
||||
f"{'succeeded' if last['success'] else 'failed'} "
|
||||
f"({last['duration_ms']:.1f} ms{item_label})"
|
||||
)
|
||||
sidekick = value.get("last_sidekick") or {}
|
||||
if sidekick:
|
||||
state = "finished" if sidekick.get("stopped_at") else "started"
|
||||
subagent = value.get("last_subagent") or {}
|
||||
if subagent:
|
||||
state = "finished" if subagent.get("stopped_at") else "started"
|
||||
print(
|
||||
"Last sidekick: "
|
||||
f"{state}, received {sidekick['context_chars']} characters of memory, "
|
||||
f"agent {sidekick['agent_id']}"
|
||||
"Last subagent: "
|
||||
f"{state}, received {subagent['context_chars']} characters of memory, "
|
||||
f"agent {subagent['agent_id']}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -524,7 +524,7 @@ def _checkpoint_message(event: dict[str, Any]) -> str:
|
||||
if text:
|
||||
return text
|
||||
return redact(payload.get("text", "")).strip()
|
||||
if kind == "sidekick_stop":
|
||||
if kind in {"subagent_stop", "sidekick_stop"}:
|
||||
return redact(payload.get("final_message", "")).strip()
|
||||
return ""
|
||||
|
||||
@@ -596,6 +596,7 @@ class EvidenceStore:
|
||||
self.conn.close()
|
||||
|
||||
def _migrate(self) -> None:
|
||||
# Keep the legacy table name so existing databases and in-flight workers remain compatible.
|
||||
self.conn.executescript(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
@@ -688,8 +689,7 @@ class EvidenceStore:
|
||||
|
||||
"""
|
||||
)
|
||||
# Remove the pre-0.1.1 no-tools snapshot implementation. The real coding
|
||||
# sidekick is a native Claude Code agent and stores no state in this DB.
|
||||
# Remove the pre-0.1.1 snapshot implementation.
|
||||
self.conn.executescript(
|
||||
"""
|
||||
DROP TABLE IF EXISTS sidekick_calls;
|
||||
@@ -1041,7 +1041,7 @@ class EvidenceStore:
|
||||
if row["memory_text"]
|
||||
]
|
||||
|
||||
def start_sidekick(
|
||||
def start_subagent(
|
||||
self,
|
||||
repo: RepoContext,
|
||||
session_id: str,
|
||||
@@ -1049,7 +1049,7 @@ class EvidenceStore:
|
||||
agent_type: str,
|
||||
context_chars: int,
|
||||
) -> bool:
|
||||
"""Record one native sidekick instance and whether context was first sent."""
|
||||
"""Record one native subagent instance and whether context was first sent."""
|
||||
with self.conn:
|
||||
cursor = self.conn.execute(
|
||||
"""INSERT OR IGNORE INTO sidekick_runs
|
||||
@@ -1067,7 +1067,7 @@ class EvidenceStore:
|
||||
)
|
||||
return int(cursor.rowcount) > 0
|
||||
|
||||
def stop_sidekick(
|
||||
def stop_subagent(
|
||||
self,
|
||||
repo: RepoContext,
|
||||
session_id: str,
|
||||
@@ -1222,7 +1222,7 @@ class EvidenceStore:
|
||||
cursor = self.conn.execute(
|
||||
f"DELETE FROM {table} WHERE {column} = ?", (repo_id,)
|
||||
)
|
||||
removed[table] = max(int(cursor.rowcount), 0)
|
||||
removed["subagent_runs" if table == "sidekick_runs" else table] = max(int(cursor.rowcount), 0)
|
||||
return removed
|
||||
|
||||
def status(self, repo_id: str) -> dict[str, Any]:
|
||||
@@ -1238,7 +1238,7 @@ class EvidenceStore:
|
||||
FROM operations WHERE repo_id = ? ORDER BY id DESC LIMIT 1""",
|
||||
(repo_id,),
|
||||
).fetchone()
|
||||
last_sidekick = self.conn.execute(
|
||||
last_subagent = self.conn.execute(
|
||||
"""SELECT session_id, agent_id, agent_type, started_at, stopped_at,
|
||||
context_chars
|
||||
FROM sidekick_runs WHERE repo_id = ?
|
||||
@@ -1250,9 +1250,9 @@ class EvidenceStore:
|
||||
"events": count("events"),
|
||||
"flushes": count("flushes"),
|
||||
"retrievals": count("retrievals"),
|
||||
"sidekick_runs": count("sidekick_runs"),
|
||||
"subagent_runs": count("sidekick_runs"),
|
||||
"last_operation": dict(last_operation) if last_operation else None,
|
||||
"last_sidekick": dict(last_sidekick) if last_sidekick else None,
|
||||
"last_subagent": dict(last_subagent) if last_subagent else None,
|
||||
}
|
||||
|
||||
|
||||
@@ -1324,7 +1324,7 @@ def tool_payload(hook_input: dict[str, Any], *, failed: bool | None = False) ->
|
||||
"tool": name,
|
||||
"failed": failed,
|
||||
"duration_ms": hook_input.get("duration_ms"),
|
||||
"agent_role": "sidekick" if hook_input.get("agent_id") else "main",
|
||||
"agent_role": "subagent" if hook_input.get("agent_id") else "main",
|
||||
}
|
||||
if hook_input.get("agent_id"):
|
||||
payload["agent_id"] = bounded(hook_input["agent_id"], 200)
|
||||
@@ -1384,35 +1384,33 @@ def record_tool(
|
||||
)
|
||||
|
||||
|
||||
def record_sidekick_start(
|
||||
store: EvidenceStore, hook_input: dict[str, Any], *, inject_context: bool = True
|
||||
def record_subagent_start(
|
||||
store: EvidenceStore, hook_input: dict[str, Any]
|
||||
) -> str:
|
||||
"""Record a native sidekick and reuse the main turn's retrieved memories."""
|
||||
"""Record a native subagent and reuse the main turn's retrieved memories."""
|
||||
session_id = _session_id(hook_input)
|
||||
repo = store.repo_for_session(session_id, hook_input.get("cwd"))
|
||||
agent_id = bounded(hook_input.get("agent_id", "unknown-agent"), 200)
|
||||
agent_type = bounded(hook_input.get("agent_type", "mem0:sidekick"), 200)
|
||||
agent_type = bounded(hook_input.get("agent_type", "unknown-agent"), 200)
|
||||
context = combine_context(
|
||||
format_context(store.injected_memories(session_id, repo.identity))
|
||||
)
|
||||
if not inject_context:
|
||||
context = ""
|
||||
first_start = store.start_sidekick(
|
||||
first_start = store.start_subagent(
|
||||
repo, session_id, agent_id, agent_type, len(context)
|
||||
)
|
||||
store.record_event(
|
||||
repo,
|
||||
session_id,
|
||||
"sidekick_start",
|
||||
"subagent_start",
|
||||
{
|
||||
"agent_id": agent_id,
|
||||
"agent_type": agent_type,
|
||||
"context_chars": len(context) if first_start else 0,
|
||||
"worktree_root": bounded(repo.root, 2000),
|
||||
"repo_root": bounded(repo.root, 2000),
|
||||
},
|
||||
)
|
||||
telemetry.record(
|
||||
"sidekick",
|
||||
"subagent",
|
||||
repo=repo,
|
||||
session_id=session_id,
|
||||
phase="start",
|
||||
@@ -1422,14 +1420,14 @@ def record_sidekick_start(
|
||||
return context if first_start else ""
|
||||
|
||||
|
||||
def record_sidekick_stop(store: EvidenceStore, hook_input: dict[str, Any]) -> None:
|
||||
def record_subagent_stop(store: EvidenceStore, hook_input: dict[str, Any]) -> None:
|
||||
session_id = _session_id(hook_input)
|
||||
repo = store.repo_for_session(session_id, hook_input.get("cwd"))
|
||||
agent_type = bounded(hook_input.get("agent_type", "mem0:sidekick"), 200)
|
||||
agent_type = bounded(hook_input.get("agent_type", "unknown-agent"), 200)
|
||||
agent_id = bounded(hook_input.get("agent_id", ""), 200)
|
||||
final_message = redact(hook_input.get("last_assistant_message", "")).strip()
|
||||
transcript_path = bounded(hook_input.get("agent_transcript_path", ""), 2000)
|
||||
agent_id = store.stop_sidekick(
|
||||
agent_id = store.stop_subagent(
|
||||
repo,
|
||||
session_id,
|
||||
agent_id,
|
||||
@@ -1440,7 +1438,7 @@ def record_sidekick_stop(store: EvidenceStore, hook_input: dict[str, Any]) -> No
|
||||
store.record_event(
|
||||
repo,
|
||||
session_id,
|
||||
"sidekick_stop",
|
||||
"subagent_stop",
|
||||
{
|
||||
"agent_id": agent_id,
|
||||
"agent_type": agent_type,
|
||||
@@ -1449,7 +1447,7 @@ def record_sidekick_stop(store: EvidenceStore, hook_input: dict[str, Any]) -> No
|
||||
},
|
||||
)
|
||||
telemetry.record(
|
||||
"sidekick",
|
||||
"subagent",
|
||||
repo=repo,
|
||||
session_id=session_id,
|
||||
phase="stop",
|
||||
@@ -1513,10 +1511,10 @@ def build_episode(
|
||||
for e in events
|
||||
if e["kind"] == "assistant_stop" and e["payload"].get("text")
|
||||
]
|
||||
sidekick_outcomes = [
|
||||
subagent_outcomes = [
|
||||
redact(e["payload"].get("final_message", "")).strip()
|
||||
for e in events
|
||||
if e["kind"] == "sidekick_stop" and e["payload"].get("final_message")
|
||||
if e["kind"] in {"subagent_stop", "sidekick_stop"} and e["payload"].get("final_message")
|
||||
]
|
||||
tools = [
|
||||
e["payload"] for e in events if e["kind"] in {"tool_result", "tool_failure"}
|
||||
@@ -1597,7 +1595,7 @@ def build_episode(
|
||||
}
|
||||
)
|
||||
pending_user_messages = []
|
||||
elif event["kind"] == "sidekick_stop":
|
||||
elif event["kind"] in {"subagent_stop", "sidekick_stop"}:
|
||||
pass
|
||||
extraction_messages.extend(pending_user_messages)
|
||||
|
||||
@@ -1613,7 +1611,7 @@ def build_episode(
|
||||
"assistant_conclusion": conclusion,
|
||||
"user_messages": prompts,
|
||||
"assistant_outcomes": assistant_conclusions,
|
||||
"sidekick_outcomes": sidekick_outcomes,
|
||||
"subagent_outcomes": subagent_outcomes,
|
||||
"extraction_messages": extraction_messages,
|
||||
"files_read": read_paths[:50],
|
||||
"files_modified": modified_paths[:50],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -10,7 +10,12 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
REPOSITORY_ROOT = ROOT.parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from build.build import build, bundle_drift, render_template, replace_output # noqa: E402
|
||||
from build.build import ( # noqa: E402
|
||||
build,
|
||||
bundle_drift,
|
||||
render_template,
|
||||
replace_output,
|
||||
)
|
||||
from build.validate import validate_bundle # noqa: E402
|
||||
|
||||
|
||||
@@ -57,6 +62,7 @@ def test_portable_bundle_is_conformant_and_self_contained(tmp_path: Path) -> Non
|
||||
assert "env" not in server
|
||||
assert not (root / "core" / "hook_runner.py").exists()
|
||||
assert not (root / "core" / "flush_worker.py").exists()
|
||||
assert not (root / "agents").exists()
|
||||
for skill in (root / "skills").glob("*/SKILL.md"):
|
||||
frontmatter = skill.read_text(encoding="utf-8").split("---", 2)[1]
|
||||
keys = {line.split(":", 1)[0] for line in frontmatter.splitlines() if ":" in line}
|
||||
@@ -73,6 +79,19 @@ def test_native_bundle_is_self_contained(host: str, tmp_path: Path) -> None:
|
||||
assert not any(path.is_symlink() for path in root.rglob("*"))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("host", ["claude-code", "cursor", "codex", "kimi", "antigravity"])
|
||||
def test_only_claude_bundles_sidekick(host: str, tmp_path: Path) -> None:
|
||||
root = build(host, "native", tmp_path / host)
|
||||
if host == "claude-code":
|
||||
agent = (root / "agents" / "sidekick.md").read_text()
|
||||
assert "model: sonnet" in agent
|
||||
assert "isolation: worktree" in agent
|
||||
else:
|
||||
assert not (root / "agents").exists()
|
||||
for path in root.rglob("*.json"):
|
||||
assert "sidekick" not in path.read_text().lower()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("host", ["claude-code", "cursor", "codex", "kimi", "antigravity"])
|
||||
def test_native_control_skills_select_the_host_store(host: str, tmp_path: Path) -> None:
|
||||
root = build(host, "native", tmp_path / host)
|
||||
|
||||
+42
-23
@@ -2,6 +2,7 @@
|
||||
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
@@ -11,11 +12,43 @@ import pytest
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "python"))
|
||||
|
||||
from memory_core import EvidenceStore # noqa: E402
|
||||
from memory_core import EvidenceStore, RepoContext, checkpoint_stats # noqa: E402
|
||||
|
||||
|
||||
@pytest.mark.parametrize("host", ["claude-code", "codex", "kimi", "cursor"])
|
||||
def test_sidekick_hooks_preserve_parent_scope_and_correlate_completion(tmp_path, host):
|
||||
def test_generic_tracking_preserves_legacy_runs_and_can_finish_them(tmp_path):
|
||||
database = tmp_path / "evidence.sqlite3"
|
||||
with sqlite3.connect(database) as connection:
|
||||
connection.executescript("""
|
||||
CREATE TABLE sidekick_runs (
|
||||
repo_id TEXT NOT NULL, session_id TEXT NOT NULL,
|
||||
agent_id TEXT NOT NULL, agent_type TEXT NOT NULL,
|
||||
started_at TEXT NOT NULL, stopped_at TEXT, transcript_path TEXT,
|
||||
context_chars INTEGER NOT NULL DEFAULT 0, final_message TEXT,
|
||||
PRIMARY KEY(repo_id, session_id, agent_id)
|
||||
);
|
||||
INSERT INTO sidekick_runs VALUES
|
||||
('repo', 'session', 'worker', 'default', '2026-09-10', NULL, NULL, 123, NULL);
|
||||
""")
|
||||
for _ in range(2):
|
||||
store = EvidenceStore(database)
|
||||
try:
|
||||
assert store.status("repo")["subagent_runs"] == 1
|
||||
row = store.conn.execute("SELECT * FROM sidekick_runs").fetchone()
|
||||
assert row["agent_id"] == "worker"
|
||||
assert row["context_chars"] == 123
|
||||
repo = RepoContext(str(tmp_path), str(tmp_path), "repo", "app", "main", "")
|
||||
store.stop_subagent(repo, "session", "worker", "default", "", "Completed after upgrade.")
|
||||
assert store.status("repo")["last_subagent"]["stopped_at"]
|
||||
assert store.conn.execute("SELECT final_message FROM sidekick_runs").fetchone()[0] == (
|
||||
"Completed after upgrade."
|
||||
)
|
||||
finally:
|
||||
store.close()
|
||||
assert checkpoint_stats([{"kind": "sidekick_stop", "payload": {"final_message": "Legacy result."}}]) == (0, 1, 14)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("host", ["claude-code", "codex"])
|
||||
def test_subagent_hooks_preserve_parent_scope_and_correlate_completion(tmp_path, host):
|
||||
parent = tmp_path / "parent"
|
||||
child = tmp_path / "child-worktree"
|
||||
parent.mkdir()
|
||||
@@ -30,21 +63,10 @@ def test_sidekick_hooks_preserve_parent_scope_and_correlate_completion(tmp_path,
|
||||
plugin = ROOT.parent / f"{host}-plugin"
|
||||
adapter = plugin / ("adapters/claude/hook.py" if host == "claude-code" else "hooks/adapter.py")
|
||||
start, stop = "sidekick-start", "sidekick-stop"
|
||||
payload = {"session_id": "parent-session", "cwd": str(child), "agent_id": "worker", "agent_type": "sidekick"}
|
||||
payload = {"session_id": "parent-session", "cwd": str(child), "agent_id": "worker", "agent_type": "default"}
|
||||
response_key = "last_assistant_message"
|
||||
if host == "kimi":
|
||||
start, stop = "SubagentStart", "SubagentStop"
|
||||
payload["agent_name"] = payload.pop("agent_type")
|
||||
response_key = "response"
|
||||
elif host == "cursor":
|
||||
start, stop = "subagentStart", "subagentStop"
|
||||
payload = {
|
||||
"conversation_id": "parent-session",
|
||||
"workspace_roots": [str(child)],
|
||||
"subagent_id": "worker",
|
||||
"subagent_type": "sidekick",
|
||||
}
|
||||
response_key = "summary"
|
||||
if host == "codex":
|
||||
start, stop = "subagent-start", "subagent-stop"
|
||||
|
||||
env = {key: value for key, value in os.environ.items() if not key.startswith(("MEM0_", "CLAUDE_PLUGIN_"))}
|
||||
env.update(MEM0_CODE_DATA_DIR=str(database.parent), MEM0_TELEMETRY="false", MEM0_API_URL="http://127.0.0.1:1")
|
||||
@@ -62,11 +84,8 @@ def test_sidekick_hooks_preserve_parent_scope_and_correlate_completion(tmp_path,
|
||||
return result.stdout
|
||||
|
||||
output = invoke(start, payload)
|
||||
if host == "cursor":
|
||||
assert json.loads(output) == {"permission": "allow"}
|
||||
else:
|
||||
context = output if host == "kimi" else json.loads(output)["hookSpecificOutput"]["additionalContext"]
|
||||
assert "Parent memory marker." in context
|
||||
context = json.loads(output)["hookSpecificOutput"]["additionalContext"]
|
||||
assert "Parent memory marker." in context
|
||||
assert "Foreign memory marker." not in output
|
||||
assert "Parent memory marker." not in invoke(start, payload)
|
||||
invoke(stop, {**payload, response_key: "Finished the delegated task."})
|
||||
@@ -80,4 +99,4 @@ def test_sidekick_hooks_preserve_parent_scope_and_correlate_completion(tmp_path,
|
||||
)
|
||||
assert runs[0]["stopped_at"]
|
||||
assert runs[0]["final_message"] == "Finished the delegated task."
|
||||
assert (runs[0]["context_chars"] > 0) == (host != "cursor")
|
||||
assert runs[0]["context_chars"] > 0
|
||||
@@ -1,3 +1,5 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
import { redactSecrets } from "./lifecycle.ts";
|
||||
|
||||
const POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX";
|
||||
@@ -74,34 +76,108 @@ export function errorKind(error: unknown): string {
|
||||
return error instanceof Error ? error.constructor.name : "other";
|
||||
}
|
||||
|
||||
// Delivery is retried in memory, not spooled to disk, and that is a decision
|
||||
// rather than an omission. The Python core spools because its hooks are separate
|
||||
// processes that fire per tool call and exit immediately, so nothing survives
|
||||
// without a file. These plugins are loaded into a host that lives for a whole
|
||||
// session, so re-queueing covers the same transient failures without the claim
|
||||
// and lease machinery a correct cross-process spool needs. What that leaves
|
||||
// uncovered is narrow: a session that both starts and ends with no connectivity.
|
||||
const RETRY_BACKOFF_CEILING_MS = 60_000;
|
||||
// Consecutive failed flushes before the queue is dropped. Deliberately NOT the
|
||||
// same thing as Python's budget, which rides in the claim filename and so
|
||||
// follows one batch: this counter lives in the closure and counts the outage,
|
||||
// not the payload. Events captured between attempts join the same queue and go
|
||||
// with it. Per-batch accounting would need an attempt count on every event, and
|
||||
// the queue is already bounded, so the simpler rule is the one in force here.
|
||||
// Without any bound a payload the server will never accept is retried for the
|
||||
// whole session and, now that the backlog is preferred over new events, holds
|
||||
// the queue against everything behind it.
|
||||
const MAX_DELIVERY_ATTEMPTS = 5;
|
||||
|
||||
export function createTelemetry(config: TelemetryConfig) {
|
||||
let queue: Record<string, unknown>[] = [];
|
||||
let timer: ReturnType<typeof setInterval> | undefined;
|
||||
let consecutiveFailures = 0;
|
||||
let retryNotBefore = 0;
|
||||
let exitFlushAttempted = false;
|
||||
let flushing = false;
|
||||
const flushThreshold = config.flushThreshold ?? 10;
|
||||
const maxQueueSize = config.maxQueueSize ?? 100;
|
||||
|
||||
const deliver = config.delivery ?? (async (batch: Record<string, unknown>[]) => {
|
||||
await fetch(POSTHOG_BATCH_URL, {
|
||||
const response = await fetch(POSTHOG_BATCH_URL, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ api_key: POSTHOG_API_KEY, batch }),
|
||||
signal: AbortSignal.timeout(3_000),
|
||||
});
|
||||
// fetch only rejects on a network-level failure. Without this check a 500,
|
||||
// a 503 or a 429 resolved normally and the batch was counted as delivered
|
||||
// and dropped, which is the likelier outage than a refused connection.
|
||||
// Any non-2xx is retried, matching the Python core: the backoff and the
|
||||
// queue bound contain a payload that will never be accepted, because the
|
||||
// re-queued batch sits at the front and is the first thing evicted.
|
||||
if (!response.ok) throw new Error(`posthog responded ${response.status}`);
|
||||
});
|
||||
|
||||
async function flush(): Promise<void> {
|
||||
async function flush(force = false): Promise<void> {
|
||||
// One at a time. Two overlapping flushes each detach the queue and each
|
||||
// prepend their own batch back on failure, so the later batch lands in front
|
||||
// of the earlier one and the truncation then drops the OLDER events first,
|
||||
// inverting the priority the failure path exists to establish. A second
|
||||
// caller returns immediately; the queue waits for the next flush.
|
||||
if (flushing) return;
|
||||
if (!queue.length) return;
|
||||
// `force` skips the cooldown. beforeExit is the last chance this process
|
||||
// gets, and gating it on the same backoff meant that after any failure the
|
||||
// exit flush did nothing and the queue died with the process, which is the
|
||||
// loss this whole mechanism exists to prevent.
|
||||
if (!force && Date.now() < retryNotBefore) return;
|
||||
const batch = queue;
|
||||
queue = [];
|
||||
flushing = true;
|
||||
try {
|
||||
await deliver(batch);
|
||||
consecutiveFailures = 0;
|
||||
retryNotBefore = 0;
|
||||
} catch {
|
||||
// Telemetry must never affect plugin behavior.
|
||||
// Put it back. Detaching the batch and swallowing the error deleted the
|
||||
// events outright, so any blip silently dropped telemetry with nothing
|
||||
// recording that it had happened. Every event carries a uuid, so a retry
|
||||
// that duplicates one PostHog already accepted is collapsed there.
|
||||
//
|
||||
consecutiveFailures += 1;
|
||||
if (consecutiveFailures >= MAX_DELIVERY_ATTEMPTS) {
|
||||
// Give up on the queue so a failing outage cannot hold it for the
|
||||
// session. This drops whatever is queued now, which includes events
|
||||
// captured during the outage, not only the batch that kept failing.
|
||||
consecutiveFailures = 0;
|
||||
retryNotBefore = 0;
|
||||
return;
|
||||
}
|
||||
// Keep the FRONT on overflow, so the batch being retried survives and a
|
||||
// new event is what gets dropped. Matches the Python core, where record()
|
||||
// refuses new events once the spool is full rather than evicting the
|
||||
// backlog. Keeping the newest would throw away exactly the events this
|
||||
// retry exists to save.
|
||||
queue = [...batch, ...queue].slice(0, maxQueueSize);
|
||||
retryNotBefore = Date.now() + Math.min(2 ** consecutiveFailures * 1_000, RETRY_BACKOFF_CEILING_MS);
|
||||
} finally {
|
||||
flushing = false;
|
||||
}
|
||||
}
|
||||
|
||||
function beforeExit(): void {
|
||||
void flush();
|
||||
// Once, and only once. Node re-emits beforeExit whenever the handler
|
||||
// schedules more async work, so an unconditional forced flush looped until
|
||||
// the attempt budget was spent: five attempts against a 3s delivery timeout
|
||||
// is fifteen seconds added to the shutdown of whatever editor or CLI is
|
||||
// hosting this. The backoff used to end that loop after one attempt, and
|
||||
// removing it for the forced path removed the only thing bounding it.
|
||||
if (exitFlushAttempted) return;
|
||||
exitFlushAttempted = true;
|
||||
void flush(true);
|
||||
}
|
||||
|
||||
function build(event: string, properties: Record<string, unknown> = {}): Record<string, unknown> | null {
|
||||
@@ -112,6 +188,15 @@ export function createTelemetry(config: TelemetryConfig) {
|
||||
return {
|
||||
event: config.eventName?.(event) ?? event,
|
||||
distinct_id: distinctId,
|
||||
// Stamped once, at capture. This is what makes retrying safe: a batch
|
||||
// re-sent after a failure carries the same ids, so PostHog collapses
|
||||
// anything it already accepted instead of counting it twice.
|
||||
uuid: randomUUID(),
|
||||
// Capture time, not ingestion time. Events now sit through backoff and
|
||||
// across a whole outage, so without this PostHog records them whenever
|
||||
// delivery happened to succeed. It also matters for the uuid dedupe
|
||||
// above, whose key includes the event date.
|
||||
timestamp: new Date().toISOString(),
|
||||
properties: {
|
||||
...safeProperties(properties),
|
||||
...safeProperties(config.commonProperties ?? {}),
|
||||
@@ -134,8 +219,10 @@ export function createTelemetry(config: TelemetryConfig) {
|
||||
try {
|
||||
const payload = build(event, properties);
|
||||
if (!payload) return;
|
||||
// Full means drop this event, not evict the backlog. Same rule as the
|
||||
// failure path above and as Python's record().
|
||||
if (queue.length >= maxQueueSize) return;
|
||||
queue.push(payload);
|
||||
if (queue.length > maxQueueSize) queue = queue.slice(-maxQueueSize);
|
||||
if (!timer) {
|
||||
timer = setInterval(() => void flush(), config.flushIntervalMs ?? 5_000);
|
||||
timer.unref?.();
|
||||
@@ -149,6 +236,8 @@ export function createTelemetry(config: TelemetryConfig) {
|
||||
|
||||
function resetForTesting(): void {
|
||||
queue = [];
|
||||
consecutiveFailures = 0;
|
||||
retryNotBefore = 0;
|
||||
if (timer) clearInterval(timer);
|
||||
timer = undefined;
|
||||
process.off("beforeExit", beforeExit);
|
||||
|
||||
@@ -122,3 +122,250 @@ test("error classification does not expose messages", () => {
|
||||
assert.equal(errorKind(new Error("request timeout")), "timeout");
|
||||
assert.equal(errorKind(new Error("fetch failed")), "network");
|
||||
});
|
||||
|
||||
test("a failed delivery keeps the batch instead of deleting it", async () => {
|
||||
// The defect: the queue was detached before the await and the error swallowed,
|
||||
// so one blip destroyed the events with nothing recording that it happened.
|
||||
const attempts: Record<string, unknown>[][] = [];
|
||||
let failNext = true;
|
||||
const telemetry = createTelemetry({
|
||||
host: "h", source: "S", version: "1", distinctId: "d",
|
||||
flushThreshold: 1000,
|
||||
delivery: async (batch) => {
|
||||
attempts.push(batch);
|
||||
if (failNext) throw new Error("network down");
|
||||
},
|
||||
});
|
||||
|
||||
telemetry.capture("one");
|
||||
telemetry.capture("two");
|
||||
await telemetry.flush();
|
||||
|
||||
assert.equal(attempts.length, 1);
|
||||
assert.equal(telemetry.queueForTesting().length, 2, "events were dropped on failure");
|
||||
|
||||
failNext = false;
|
||||
// Backoff is in force, so wait it out the way wall time would.
|
||||
await new Promise((resolve) => setTimeout(resolve, 2_100));
|
||||
await telemetry.flush();
|
||||
|
||||
assert.equal(attempts.length, 2, "never retried");
|
||||
assert.equal(telemetry.queueForTesting().length, 0);
|
||||
telemetry.resetForTesting();
|
||||
});
|
||||
|
||||
test("a retried event carries the same uuid so PostHog can collapse it", async () => {
|
||||
const attempts: Record<string, unknown>[][] = [];
|
||||
let failNext = true;
|
||||
const telemetry = createTelemetry({
|
||||
host: "h", source: "S", version: "1", distinctId: "d",
|
||||
flushThreshold: 1000,
|
||||
delivery: async (batch) => {
|
||||
attempts.push(batch);
|
||||
if (failNext) throw new Error("network down");
|
||||
},
|
||||
});
|
||||
|
||||
telemetry.capture("once");
|
||||
await telemetry.flush();
|
||||
failNext = false;
|
||||
await new Promise((resolve) => setTimeout(resolve, 2_100));
|
||||
await telemetry.flush();
|
||||
|
||||
assert.equal(attempts.length, 2);
|
||||
const first = attempts[0][0].uuid;
|
||||
assert.ok(first, "events carry no uuid, so a retry would double count");
|
||||
assert.equal(attempts[1][0].uuid, first, "retry minted a new uuid");
|
||||
telemetry.resetForTesting();
|
||||
});
|
||||
|
||||
test("repeated failures back off instead of retrying every flush", async () => {
|
||||
let calls = 0;
|
||||
const telemetry = createTelemetry({
|
||||
host: "h", source: "S", version: "1", distinctId: "d",
|
||||
flushThreshold: 1000,
|
||||
delivery: async () => { calls += 1; throw new Error("blocked"); },
|
||||
});
|
||||
|
||||
telemetry.capture("one");
|
||||
await telemetry.flush();
|
||||
await telemetry.flush();
|
||||
await telemetry.flush();
|
||||
|
||||
assert.equal(calls, 1, "a blocked host was hammered on every flush");
|
||||
assert.equal(telemetry.queueForTesting().length, 1, "the event was lost while backing off");
|
||||
telemetry.resetForTesting();
|
||||
});
|
||||
|
||||
test("a full queue drops the new event and keeps the batch being retried", async () => {
|
||||
// Python's record() refuses new events once the spool is full rather than
|
||||
// evicting the backlog. Keeping the newest here would throw away exactly the
|
||||
// events the retry exists to save.
|
||||
const telemetry = createTelemetry({
|
||||
host: "h", source: "S", version: "1", distinctId: "d",
|
||||
flushThreshold: 1000, maxQueueSize: 3,
|
||||
delivery: async () => { throw new Error("down"); },
|
||||
});
|
||||
|
||||
// Fill past the cap BEFORE the flush, so the re-queue actually has to truncate.
|
||||
// Capturing only two left the queue empty at re-queue time and the slice on the
|
||||
// failure path never ran, which is the half that decides the direction.
|
||||
telemetry.capture("a");
|
||||
telemetry.capture("b");
|
||||
telemetry.capture("c");
|
||||
await telemetry.flush();
|
||||
telemetry.capture("d");
|
||||
telemetry.capture("e");
|
||||
|
||||
const events = telemetry.queueForTesting().map((e) => (e as any).event);
|
||||
assert.equal(events.length, 3, "queue grew past maxQueueSize");
|
||||
assert.deepEqual(events, ["a", "b", "c"], "the retried batch was evicted instead of the new events");
|
||||
telemetry.resetForTesting();
|
||||
});
|
||||
|
||||
test("the exit-time flush ignores the backoff", async () => {
|
||||
// beforeExit is the last chance the process gets. Gating it on the same
|
||||
// cooldown meant that after any failure it did nothing and the queue died.
|
||||
let attempts = 0;
|
||||
let failing = true;
|
||||
const telemetry = createTelemetry({
|
||||
host: "h", source: "S", version: "1", distinctId: "d", flushThreshold: 1000,
|
||||
delivery: async () => { attempts += 1; if (failing) throw new Error("down"); },
|
||||
});
|
||||
|
||||
telemetry.capture("a");
|
||||
await telemetry.flush();
|
||||
assert.equal(attempts, 1);
|
||||
|
||||
failing = false;
|
||||
await telemetry.flush();
|
||||
assert.equal(attempts, 1, "the backoff should still hold for an ordinary flush");
|
||||
|
||||
await telemetry.flush(true);
|
||||
assert.equal(attempts, 2, "the exit flush was suppressed by the backoff");
|
||||
assert.equal(telemetry.queueForTesting().length, 0);
|
||||
telemetry.resetForTesting();
|
||||
});
|
||||
|
||||
test("every event carries a capture-time timestamp", async () => {
|
||||
const sent: Record<string, unknown>[][] = [];
|
||||
const telemetry = createTelemetry({
|
||||
host: "h", source: "S", version: "1", distinctId: "d", flushThreshold: 1000,
|
||||
delivery: async (batch) => { sent.push(batch); },
|
||||
});
|
||||
|
||||
telemetry.capture("a");
|
||||
const capturedAt = Date.now();
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
await telemetry.flush();
|
||||
|
||||
const stamped = sent[0][0].timestamp as string;
|
||||
assert.ok(stamped, "no timestamp, so PostHog would record delivery time");
|
||||
assert.ok(Math.abs(Date.parse(stamped) - capturedAt) < 1_000, "not capture time");
|
||||
telemetry.resetForTesting();
|
||||
});
|
||||
|
||||
test("a batch the server will never accept is eventually given up on", async () => {
|
||||
let attempts = 0;
|
||||
const telemetry = createTelemetry({
|
||||
host: "h", source: "S", version: "1", distinctId: "d", flushThreshold: 1000,
|
||||
delivery: async () => { attempts += 1; throw new Error("permanently bad"); },
|
||||
});
|
||||
|
||||
telemetry.capture("doomed");
|
||||
for (let i = 0; i < 8; i += 1) await telemetry.flush(true);
|
||||
|
||||
assert.ok(attempts <= 6, `retried ${attempts} times with no cap`);
|
||||
assert.equal(telemetry.queueForTesting().length, 0, "a doomed batch held the queue forever");
|
||||
telemetry.resetForTesting();
|
||||
});
|
||||
|
||||
test("an HTTP error response is a failure, not a delivery", async () => {
|
||||
// fetch only rejects on a network-level failure, so a 500 used to resolve
|
||||
// normally and the batch was dropped as delivered. Exercises the real default
|
||||
// delivery path rather than an injected one, which is where this hid.
|
||||
const realFetch = globalThis.fetch;
|
||||
let calls = 0;
|
||||
globalThis.fetch = (async () => {
|
||||
calls += 1;
|
||||
return new Response("upstream is unwell", { status: 503 });
|
||||
}) as typeof fetch;
|
||||
|
||||
const telemetry = createTelemetry({
|
||||
host: "h", source: "S", version: "1", distinctId: "d", flushThreshold: 1000,
|
||||
});
|
||||
try {
|
||||
telemetry.capture("during.outage");
|
||||
await telemetry.flush();
|
||||
|
||||
assert.equal(calls, 1, "never reached the network");
|
||||
assert.equal(telemetry.queueForTesting().length, 1, "a 503 was counted as delivered");
|
||||
} finally {
|
||||
globalThis.fetch = realFetch;
|
||||
telemetry.resetForTesting();
|
||||
}
|
||||
});
|
||||
|
||||
test("a 2xx is a delivery", async () => {
|
||||
const realFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async () => new Response("ok", { status: 200 })) as typeof fetch;
|
||||
|
||||
const telemetry = createTelemetry({
|
||||
host: "h", source: "S", version: "1", distinctId: "d", flushThreshold: 1000,
|
||||
});
|
||||
try {
|
||||
telemetry.capture("fine");
|
||||
await telemetry.flush();
|
||||
assert.equal(telemetry.queueForTesting().length, 0, "a good response did not clear the queue");
|
||||
} finally {
|
||||
globalThis.fetch = realFetch;
|
||||
telemetry.resetForTesting();
|
||||
}
|
||||
});
|
||||
|
||||
test("the exit flush is attempted once, not until the budget is spent", async () => {
|
||||
// Node re-emits beforeExit whenever the handler schedules async work, so an
|
||||
// unconditional forced flush looped until MAX_DELIVERY_ATTEMPTS. Against the
|
||||
// real 3s delivery timeout that is fifteen seconds added to a host's shutdown.
|
||||
let attempts = 0;
|
||||
const telemetry = createTelemetry({
|
||||
host: "h", source: "S", version: "1", distinctId: "d", flushThreshold: 1000,
|
||||
delivery: async () => { attempts += 1; throw new Error("down"); },
|
||||
});
|
||||
|
||||
telemetry.capture("a");
|
||||
const handlers = process.listeners("beforeExit");
|
||||
const ours = handlers[handlers.length - 1] as () => void;
|
||||
ours();
|
||||
ours();
|
||||
ours();
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
|
||||
assert.equal(attempts, 1, `exit flush ran ${attempts} times`);
|
||||
telemetry.resetForTesting();
|
||||
});
|
||||
|
||||
test("overlapping flushes do not reorder the backlog behind newer events", async () => {
|
||||
// Each flush detaches the queue and prepends its own batch back on failure, so
|
||||
// two in flight at once put the LATER batch in front of the earlier one. The
|
||||
// truncation then drops the older events first, inverting the priority the
|
||||
// failure path exists to establish.
|
||||
let release: (() => void)[] = [];
|
||||
const telemetry = createTelemetry({
|
||||
host: "h", source: "S", version: "1", distinctId: "d", flushThreshold: 1000,
|
||||
delivery: () => new Promise((_resolve, reject) => { release.push(() => reject(new Error("down"))); }),
|
||||
});
|
||||
|
||||
telemetry.capture("first");
|
||||
const a = telemetry.flush();
|
||||
telemetry.capture("second");
|
||||
const b = telemetry.flush();
|
||||
|
||||
release.forEach((fn) => fn());
|
||||
await Promise.all([a, b]);
|
||||
|
||||
const events = telemetry.queueForTesting().map((e) => (e as any).event);
|
||||
assert.equal(release.length, 1, "a second delivery started while one was in flight");
|
||||
assert.deepEqual(events, ["first", "second"], `backlog reordered: ${events.join(",")}`);
|
||||
telemetry.resetForTesting();
|
||||
});
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
name: sidekick
|
||||
description: Coding subagent for focused implementation, investigation, testing, debugging, or review work.
|
||||
subagent: true
|
||||
---
|
||||
|
||||
You are Mem0's coding sidekick. Complete only the bounded task the main agent
|
||||
delegates to you and return a concise, self-contained result.
|
||||
|
||||
Search Mem0 before work that may depend on prior repository decisions or user
|
||||
preferences. Inspect the relevant repository rules and code, make changes when
|
||||
asked, and run the smallest decisive validation. Use only the workspace the
|
||||
caller assigned; do not assume a separate Git worktree.
|
||||
|
||||
Your final response must state the outcome, changed files, validation, and any
|
||||
remaining risk. Do not commit, push, or open a pull request unless the caller
|
||||
explicitly asks.
|
||||
@@ -11,10 +11,10 @@ import telemetry
|
||||
from memory_core import (
|
||||
EvidenceStore,
|
||||
api_key,
|
||||
configure_harness,
|
||||
data_dir,
|
||||
doctor,
|
||||
forget_remote_repo,
|
||||
configure_harness,
|
||||
resolve_repo,
|
||||
user_id,
|
||||
)
|
||||
@@ -32,7 +32,7 @@ def _print_status(value: dict) -> None:
|
||||
)
|
||||
print(
|
||||
f"Used in this repository: {value['retrievals']} memories returned, "
|
||||
f"{value['sidekick_runs']} sidekick runs"
|
||||
f"{value['subagent_runs']} subagent runs"
|
||||
)
|
||||
if last:
|
||||
item_label = ""
|
||||
@@ -48,13 +48,13 @@ def _print_status(value: dict) -> None:
|
||||
f"{'succeeded' if last['success'] else 'failed'} "
|
||||
f"({last['duration_ms']:.1f} ms{item_label})"
|
||||
)
|
||||
sidekick = value.get("last_sidekick") or {}
|
||||
if sidekick:
|
||||
state = "finished" if sidekick.get("stopped_at") else "started"
|
||||
subagent = value.get("last_subagent") or {}
|
||||
if subagent:
|
||||
state = "finished" if subagent.get("stopped_at") else "started"
|
||||
print(
|
||||
"Last sidekick: "
|
||||
f"{state}, received {sidekick['context_chars']} characters of memory, "
|
||||
f"agent {sidekick['agent_id']}"
|
||||
"Last subagent: "
|
||||
f"{state}, received {subagent['context_chars']} characters of memory, "
|
||||
f"agent {subagent['agent_id']}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -524,7 +524,7 @@ def _checkpoint_message(event: dict[str, Any]) -> str:
|
||||
if text:
|
||||
return text
|
||||
return redact(payload.get("text", "")).strip()
|
||||
if kind == "sidekick_stop":
|
||||
if kind in {"subagent_stop", "sidekick_stop"}:
|
||||
return redact(payload.get("final_message", "")).strip()
|
||||
return ""
|
||||
|
||||
@@ -596,6 +596,7 @@ class EvidenceStore:
|
||||
self.conn.close()
|
||||
|
||||
def _migrate(self) -> None:
|
||||
# Keep the legacy table name so existing databases and in-flight workers remain compatible.
|
||||
self.conn.executescript(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
@@ -688,8 +689,7 @@ class EvidenceStore:
|
||||
|
||||
"""
|
||||
)
|
||||
# Remove the pre-0.1.1 no-tools snapshot implementation. The real coding
|
||||
# sidekick is a native Claude Code agent and stores no state in this DB.
|
||||
# Remove the pre-0.1.1 snapshot implementation.
|
||||
self.conn.executescript(
|
||||
"""
|
||||
DROP TABLE IF EXISTS sidekick_calls;
|
||||
@@ -1041,7 +1041,7 @@ class EvidenceStore:
|
||||
if row["memory_text"]
|
||||
]
|
||||
|
||||
def start_sidekick(
|
||||
def start_subagent(
|
||||
self,
|
||||
repo: RepoContext,
|
||||
session_id: str,
|
||||
@@ -1049,7 +1049,7 @@ class EvidenceStore:
|
||||
agent_type: str,
|
||||
context_chars: int,
|
||||
) -> bool:
|
||||
"""Record one native sidekick instance and whether context was first sent."""
|
||||
"""Record one native subagent instance and whether context was first sent."""
|
||||
with self.conn:
|
||||
cursor = self.conn.execute(
|
||||
"""INSERT OR IGNORE INTO sidekick_runs
|
||||
@@ -1067,7 +1067,7 @@ class EvidenceStore:
|
||||
)
|
||||
return int(cursor.rowcount) > 0
|
||||
|
||||
def stop_sidekick(
|
||||
def stop_subagent(
|
||||
self,
|
||||
repo: RepoContext,
|
||||
session_id: str,
|
||||
@@ -1222,7 +1222,7 @@ class EvidenceStore:
|
||||
cursor = self.conn.execute(
|
||||
f"DELETE FROM {table} WHERE {column} = ?", (repo_id,)
|
||||
)
|
||||
removed[table] = max(int(cursor.rowcount), 0)
|
||||
removed["subagent_runs" if table == "sidekick_runs" else table] = max(int(cursor.rowcount), 0)
|
||||
return removed
|
||||
|
||||
def status(self, repo_id: str) -> dict[str, Any]:
|
||||
@@ -1238,7 +1238,7 @@ class EvidenceStore:
|
||||
FROM operations WHERE repo_id = ? ORDER BY id DESC LIMIT 1""",
|
||||
(repo_id,),
|
||||
).fetchone()
|
||||
last_sidekick = self.conn.execute(
|
||||
last_subagent = self.conn.execute(
|
||||
"""SELECT session_id, agent_id, agent_type, started_at, stopped_at,
|
||||
context_chars
|
||||
FROM sidekick_runs WHERE repo_id = ?
|
||||
@@ -1250,9 +1250,9 @@ class EvidenceStore:
|
||||
"events": count("events"),
|
||||
"flushes": count("flushes"),
|
||||
"retrievals": count("retrievals"),
|
||||
"sidekick_runs": count("sidekick_runs"),
|
||||
"subagent_runs": count("sidekick_runs"),
|
||||
"last_operation": dict(last_operation) if last_operation else None,
|
||||
"last_sidekick": dict(last_sidekick) if last_sidekick else None,
|
||||
"last_subagent": dict(last_subagent) if last_subagent else None,
|
||||
}
|
||||
|
||||
|
||||
@@ -1324,7 +1324,7 @@ def tool_payload(hook_input: dict[str, Any], *, failed: bool | None = False) ->
|
||||
"tool": name,
|
||||
"failed": failed,
|
||||
"duration_ms": hook_input.get("duration_ms"),
|
||||
"agent_role": "sidekick" if hook_input.get("agent_id") else "main",
|
||||
"agent_role": "subagent" if hook_input.get("agent_id") else "main",
|
||||
}
|
||||
if hook_input.get("agent_id"):
|
||||
payload["agent_id"] = bounded(hook_input["agent_id"], 200)
|
||||
@@ -1384,35 +1384,33 @@ def record_tool(
|
||||
)
|
||||
|
||||
|
||||
def record_sidekick_start(
|
||||
store: EvidenceStore, hook_input: dict[str, Any], *, inject_context: bool = True
|
||||
def record_subagent_start(
|
||||
store: EvidenceStore, hook_input: dict[str, Any]
|
||||
) -> str:
|
||||
"""Record a native sidekick and reuse the main turn's retrieved memories."""
|
||||
"""Record a native subagent and reuse the main turn's retrieved memories."""
|
||||
session_id = _session_id(hook_input)
|
||||
repo = store.repo_for_session(session_id, hook_input.get("cwd"))
|
||||
agent_id = bounded(hook_input.get("agent_id", "unknown-agent"), 200)
|
||||
agent_type = bounded(hook_input.get("agent_type", "mem0:sidekick"), 200)
|
||||
agent_type = bounded(hook_input.get("agent_type", "unknown-agent"), 200)
|
||||
context = combine_context(
|
||||
format_context(store.injected_memories(session_id, repo.identity))
|
||||
)
|
||||
if not inject_context:
|
||||
context = ""
|
||||
first_start = store.start_sidekick(
|
||||
first_start = store.start_subagent(
|
||||
repo, session_id, agent_id, agent_type, len(context)
|
||||
)
|
||||
store.record_event(
|
||||
repo,
|
||||
session_id,
|
||||
"sidekick_start",
|
||||
"subagent_start",
|
||||
{
|
||||
"agent_id": agent_id,
|
||||
"agent_type": agent_type,
|
||||
"context_chars": len(context) if first_start else 0,
|
||||
"worktree_root": bounded(repo.root, 2000),
|
||||
"repo_root": bounded(repo.root, 2000),
|
||||
},
|
||||
)
|
||||
telemetry.record(
|
||||
"sidekick",
|
||||
"subagent",
|
||||
repo=repo,
|
||||
session_id=session_id,
|
||||
phase="start",
|
||||
@@ -1422,14 +1420,14 @@ def record_sidekick_start(
|
||||
return context if first_start else ""
|
||||
|
||||
|
||||
def record_sidekick_stop(store: EvidenceStore, hook_input: dict[str, Any]) -> None:
|
||||
def record_subagent_stop(store: EvidenceStore, hook_input: dict[str, Any]) -> None:
|
||||
session_id = _session_id(hook_input)
|
||||
repo = store.repo_for_session(session_id, hook_input.get("cwd"))
|
||||
agent_type = bounded(hook_input.get("agent_type", "mem0:sidekick"), 200)
|
||||
agent_type = bounded(hook_input.get("agent_type", "unknown-agent"), 200)
|
||||
agent_id = bounded(hook_input.get("agent_id", ""), 200)
|
||||
final_message = redact(hook_input.get("last_assistant_message", "")).strip()
|
||||
transcript_path = bounded(hook_input.get("agent_transcript_path", ""), 2000)
|
||||
agent_id = store.stop_sidekick(
|
||||
agent_id = store.stop_subagent(
|
||||
repo,
|
||||
session_id,
|
||||
agent_id,
|
||||
@@ -1440,7 +1438,7 @@ def record_sidekick_stop(store: EvidenceStore, hook_input: dict[str, Any]) -> No
|
||||
store.record_event(
|
||||
repo,
|
||||
session_id,
|
||||
"sidekick_stop",
|
||||
"subagent_stop",
|
||||
{
|
||||
"agent_id": agent_id,
|
||||
"agent_type": agent_type,
|
||||
@@ -1449,7 +1447,7 @@ def record_sidekick_stop(store: EvidenceStore, hook_input: dict[str, Any]) -> No
|
||||
},
|
||||
)
|
||||
telemetry.record(
|
||||
"sidekick",
|
||||
"subagent",
|
||||
repo=repo,
|
||||
session_id=session_id,
|
||||
phase="stop",
|
||||
@@ -1513,10 +1511,10 @@ def build_episode(
|
||||
for e in events
|
||||
if e["kind"] == "assistant_stop" and e["payload"].get("text")
|
||||
]
|
||||
sidekick_outcomes = [
|
||||
subagent_outcomes = [
|
||||
redact(e["payload"].get("final_message", "")).strip()
|
||||
for e in events
|
||||
if e["kind"] == "sidekick_stop" and e["payload"].get("final_message")
|
||||
if e["kind"] in {"subagent_stop", "sidekick_stop"} and e["payload"].get("final_message")
|
||||
]
|
||||
tools = [
|
||||
e["payload"] for e in events if e["kind"] in {"tool_result", "tool_failure"}
|
||||
@@ -1597,7 +1595,7 @@ def build_episode(
|
||||
}
|
||||
)
|
||||
pending_user_messages = []
|
||||
elif event["kind"] == "sidekick_stop":
|
||||
elif event["kind"] in {"subagent_stop", "sidekick_stop"}:
|
||||
pass
|
||||
extraction_messages.extend(pending_user_messages)
|
||||
|
||||
@@ -1613,7 +1611,7 @@ def build_episode(
|
||||
"assistant_conclusion": conclusion,
|
||||
"user_messages": prompts,
|
||||
"assistant_outcomes": assistant_conclusions,
|
||||
"sidekick_outcomes": sidekick_outcomes,
|
||||
"subagent_outcomes": subagent_outcomes,
|
||||
"extraction_messages": extraction_messages,
|
||||
"files_read": read_paths[:50],
|
||||
"files_modified": modified_paths[:50],
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
"plugin.json": "plugin.json",
|
||||
"hooks.json": "hooks.json",
|
||||
"mcp_config.json": "mcp_config.json",
|
||||
"hooks/adapter.py": "hooks/adapter.py",
|
||||
"agents/sidekick/agent.md": "agents/sidekick/agent.md"
|
||||
"hooks/adapter.py": "hooks/adapter.py"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ def test_native_antigravity_bundle_uses_supported_events(tmp_path: Path) -> None
|
||||
assert manifest["$schema"] == "https://antigravity.google/schemas/v1/plugin.json"
|
||||
assert set(hooks) == {"PreInvocation", "PostToolUse", "Stop"}
|
||||
assert (root / "mcp_config.json").is_file()
|
||||
assert (root / "agents" / "sidekick" / "agent.md").is_file()
|
||||
assert not (root / "agents").exists()
|
||||
assert not any(path.is_symlink() for path in root.rglob("*"))
|
||||
|
||||
|
||||
|
||||
@@ -63,6 +63,8 @@ After that first search, Claude can call `search_memories` with a specific quest
|
||||
|
||||
### Sonnet sidekick agent
|
||||
|
||||
Sidekick is available only in this plugin. The shared core handles memory and subagent tracking.
|
||||
|
||||
`mem0:sidekick` is a Sonnet coding agent that runs in a separate Git worktree. It can investigate, implement, test, debug, or review something instead of the main (Opus/Fable) session doing the same work, reducing cost when the main agent doesn't need to repeat it.
|
||||
|
||||
The main agent reviews the result. Corrections go back to the same sidekick so it keeps what it learned. Changes stay in the sidekick's worktree until the main agent reviews and copies them over.
|
||||
|
||||
@@ -12,22 +12,22 @@ _core_dir = _bundled_core if (_bundled_core / "memory_core.py").is_file() else _
|
||||
sys.path.insert(0, str(_core_dir))
|
||||
sys.path.insert(0, str(_here.parent))
|
||||
|
||||
import hook_runner # noqa: E402
|
||||
import telemetry # noqa: E402
|
||||
from memory_core import ( # noqa: E402
|
||||
configure_harness,
|
||||
record_sidekick_start,
|
||||
record_sidekick_stop,
|
||||
record_subagent_start,
|
||||
record_subagent_stop,
|
||||
record_tool,
|
||||
)
|
||||
from transcript import record_stop # noqa: E402
|
||||
import hook_runner # noqa: E402
|
||||
|
||||
configure_harness("claude-code", data_dir_name="claude-code-plugin", source_tag="claude_code_plugin")
|
||||
telemetry.init(harness="claude-code", source_tag="CLAUDE_CODE_PLUGIN")
|
||||
|
||||
|
||||
def _sidekick_start(store, hook_input):
|
||||
context = record_sidekick_start(store, hook_input)
|
||||
context = record_subagent_start(store, {"agent_type": "mem0:sidekick", **hook_input})
|
||||
if context:
|
||||
return {
|
||||
"hookSpecificOutput": {
|
||||
@@ -38,7 +38,7 @@ def _sidekick_start(store, hook_input):
|
||||
|
||||
|
||||
def _sidekick_stop(store, hook_input):
|
||||
record_sidekick_stop(store, hook_input)
|
||||
record_subagent_stop(store, {"agent_type": "mem0:sidekick", **hook_input})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -11,10 +11,10 @@ import telemetry
|
||||
from memory_core import (
|
||||
EvidenceStore,
|
||||
api_key,
|
||||
configure_harness,
|
||||
data_dir,
|
||||
doctor,
|
||||
forget_remote_repo,
|
||||
configure_harness,
|
||||
resolve_repo,
|
||||
user_id,
|
||||
)
|
||||
@@ -32,7 +32,7 @@ def _print_status(value: dict) -> None:
|
||||
)
|
||||
print(
|
||||
f"Used in this repository: {value['retrievals']} memories returned, "
|
||||
f"{value['sidekick_runs']} sidekick runs"
|
||||
f"{value['subagent_runs']} subagent runs"
|
||||
)
|
||||
if last:
|
||||
item_label = ""
|
||||
@@ -48,13 +48,13 @@ def _print_status(value: dict) -> None:
|
||||
f"{'succeeded' if last['success'] else 'failed'} "
|
||||
f"({last['duration_ms']:.1f} ms{item_label})"
|
||||
)
|
||||
sidekick = value.get("last_sidekick") or {}
|
||||
if sidekick:
|
||||
state = "finished" if sidekick.get("stopped_at") else "started"
|
||||
subagent = value.get("last_subagent") or {}
|
||||
if subagent:
|
||||
state = "finished" if subagent.get("stopped_at") else "started"
|
||||
print(
|
||||
"Last sidekick: "
|
||||
f"{state}, received {sidekick['context_chars']} characters of memory, "
|
||||
f"agent {sidekick['agent_id']}"
|
||||
"Last subagent: "
|
||||
f"{state}, received {subagent['context_chars']} characters of memory, "
|
||||
f"agent {subagent['agent_id']}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -524,7 +524,7 @@ def _checkpoint_message(event: dict[str, Any]) -> str:
|
||||
if text:
|
||||
return text
|
||||
return redact(payload.get("text", "")).strip()
|
||||
if kind == "sidekick_stop":
|
||||
if kind in {"subagent_stop", "sidekick_stop"}:
|
||||
return redact(payload.get("final_message", "")).strip()
|
||||
return ""
|
||||
|
||||
@@ -596,6 +596,7 @@ class EvidenceStore:
|
||||
self.conn.close()
|
||||
|
||||
def _migrate(self) -> None:
|
||||
# Keep the legacy table name so existing databases and in-flight workers remain compatible.
|
||||
self.conn.executescript(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
@@ -688,8 +689,7 @@ class EvidenceStore:
|
||||
|
||||
"""
|
||||
)
|
||||
# Remove the pre-0.1.1 no-tools snapshot implementation. The real coding
|
||||
# sidekick is a native Claude Code agent and stores no state in this DB.
|
||||
# Remove the pre-0.1.1 snapshot implementation.
|
||||
self.conn.executescript(
|
||||
"""
|
||||
DROP TABLE IF EXISTS sidekick_calls;
|
||||
@@ -1041,7 +1041,7 @@ class EvidenceStore:
|
||||
if row["memory_text"]
|
||||
]
|
||||
|
||||
def start_sidekick(
|
||||
def start_subagent(
|
||||
self,
|
||||
repo: RepoContext,
|
||||
session_id: str,
|
||||
@@ -1049,7 +1049,7 @@ class EvidenceStore:
|
||||
agent_type: str,
|
||||
context_chars: int,
|
||||
) -> bool:
|
||||
"""Record one native sidekick instance and whether context was first sent."""
|
||||
"""Record one native subagent instance and whether context was first sent."""
|
||||
with self.conn:
|
||||
cursor = self.conn.execute(
|
||||
"""INSERT OR IGNORE INTO sidekick_runs
|
||||
@@ -1067,7 +1067,7 @@ class EvidenceStore:
|
||||
)
|
||||
return int(cursor.rowcount) > 0
|
||||
|
||||
def stop_sidekick(
|
||||
def stop_subagent(
|
||||
self,
|
||||
repo: RepoContext,
|
||||
session_id: str,
|
||||
@@ -1222,7 +1222,7 @@ class EvidenceStore:
|
||||
cursor = self.conn.execute(
|
||||
f"DELETE FROM {table} WHERE {column} = ?", (repo_id,)
|
||||
)
|
||||
removed[table] = max(int(cursor.rowcount), 0)
|
||||
removed["subagent_runs" if table == "sidekick_runs" else table] = max(int(cursor.rowcount), 0)
|
||||
return removed
|
||||
|
||||
def status(self, repo_id: str) -> dict[str, Any]:
|
||||
@@ -1238,7 +1238,7 @@ class EvidenceStore:
|
||||
FROM operations WHERE repo_id = ? ORDER BY id DESC LIMIT 1""",
|
||||
(repo_id,),
|
||||
).fetchone()
|
||||
last_sidekick = self.conn.execute(
|
||||
last_subagent = self.conn.execute(
|
||||
"""SELECT session_id, agent_id, agent_type, started_at, stopped_at,
|
||||
context_chars
|
||||
FROM sidekick_runs WHERE repo_id = ?
|
||||
@@ -1250,9 +1250,9 @@ class EvidenceStore:
|
||||
"events": count("events"),
|
||||
"flushes": count("flushes"),
|
||||
"retrievals": count("retrievals"),
|
||||
"sidekick_runs": count("sidekick_runs"),
|
||||
"subagent_runs": count("sidekick_runs"),
|
||||
"last_operation": dict(last_operation) if last_operation else None,
|
||||
"last_sidekick": dict(last_sidekick) if last_sidekick else None,
|
||||
"last_subagent": dict(last_subagent) if last_subagent else None,
|
||||
}
|
||||
|
||||
|
||||
@@ -1324,7 +1324,7 @@ def tool_payload(hook_input: dict[str, Any], *, failed: bool | None = False) ->
|
||||
"tool": name,
|
||||
"failed": failed,
|
||||
"duration_ms": hook_input.get("duration_ms"),
|
||||
"agent_role": "sidekick" if hook_input.get("agent_id") else "main",
|
||||
"agent_role": "subagent" if hook_input.get("agent_id") else "main",
|
||||
}
|
||||
if hook_input.get("agent_id"):
|
||||
payload["agent_id"] = bounded(hook_input["agent_id"], 200)
|
||||
@@ -1384,35 +1384,33 @@ def record_tool(
|
||||
)
|
||||
|
||||
|
||||
def record_sidekick_start(
|
||||
store: EvidenceStore, hook_input: dict[str, Any], *, inject_context: bool = True
|
||||
def record_subagent_start(
|
||||
store: EvidenceStore, hook_input: dict[str, Any]
|
||||
) -> str:
|
||||
"""Record a native sidekick and reuse the main turn's retrieved memories."""
|
||||
"""Record a native subagent and reuse the main turn's retrieved memories."""
|
||||
session_id = _session_id(hook_input)
|
||||
repo = store.repo_for_session(session_id, hook_input.get("cwd"))
|
||||
agent_id = bounded(hook_input.get("agent_id", "unknown-agent"), 200)
|
||||
agent_type = bounded(hook_input.get("agent_type", "mem0:sidekick"), 200)
|
||||
agent_type = bounded(hook_input.get("agent_type", "unknown-agent"), 200)
|
||||
context = combine_context(
|
||||
format_context(store.injected_memories(session_id, repo.identity))
|
||||
)
|
||||
if not inject_context:
|
||||
context = ""
|
||||
first_start = store.start_sidekick(
|
||||
first_start = store.start_subagent(
|
||||
repo, session_id, agent_id, agent_type, len(context)
|
||||
)
|
||||
store.record_event(
|
||||
repo,
|
||||
session_id,
|
||||
"sidekick_start",
|
||||
"subagent_start",
|
||||
{
|
||||
"agent_id": agent_id,
|
||||
"agent_type": agent_type,
|
||||
"context_chars": len(context) if first_start else 0,
|
||||
"worktree_root": bounded(repo.root, 2000),
|
||||
"repo_root": bounded(repo.root, 2000),
|
||||
},
|
||||
)
|
||||
telemetry.record(
|
||||
"sidekick",
|
||||
"subagent",
|
||||
repo=repo,
|
||||
session_id=session_id,
|
||||
phase="start",
|
||||
@@ -1422,14 +1420,14 @@ def record_sidekick_start(
|
||||
return context if first_start else ""
|
||||
|
||||
|
||||
def record_sidekick_stop(store: EvidenceStore, hook_input: dict[str, Any]) -> None:
|
||||
def record_subagent_stop(store: EvidenceStore, hook_input: dict[str, Any]) -> None:
|
||||
session_id = _session_id(hook_input)
|
||||
repo = store.repo_for_session(session_id, hook_input.get("cwd"))
|
||||
agent_type = bounded(hook_input.get("agent_type", "mem0:sidekick"), 200)
|
||||
agent_type = bounded(hook_input.get("agent_type", "unknown-agent"), 200)
|
||||
agent_id = bounded(hook_input.get("agent_id", ""), 200)
|
||||
final_message = redact(hook_input.get("last_assistant_message", "")).strip()
|
||||
transcript_path = bounded(hook_input.get("agent_transcript_path", ""), 2000)
|
||||
agent_id = store.stop_sidekick(
|
||||
agent_id = store.stop_subagent(
|
||||
repo,
|
||||
session_id,
|
||||
agent_id,
|
||||
@@ -1440,7 +1438,7 @@ def record_sidekick_stop(store: EvidenceStore, hook_input: dict[str, Any]) -> No
|
||||
store.record_event(
|
||||
repo,
|
||||
session_id,
|
||||
"sidekick_stop",
|
||||
"subagent_stop",
|
||||
{
|
||||
"agent_id": agent_id,
|
||||
"agent_type": agent_type,
|
||||
@@ -1449,7 +1447,7 @@ def record_sidekick_stop(store: EvidenceStore, hook_input: dict[str, Any]) -> No
|
||||
},
|
||||
)
|
||||
telemetry.record(
|
||||
"sidekick",
|
||||
"subagent",
|
||||
repo=repo,
|
||||
session_id=session_id,
|
||||
phase="stop",
|
||||
@@ -1513,10 +1511,10 @@ def build_episode(
|
||||
for e in events
|
||||
if e["kind"] == "assistant_stop" and e["payload"].get("text")
|
||||
]
|
||||
sidekick_outcomes = [
|
||||
subagent_outcomes = [
|
||||
redact(e["payload"].get("final_message", "")).strip()
|
||||
for e in events
|
||||
if e["kind"] == "sidekick_stop" and e["payload"].get("final_message")
|
||||
if e["kind"] in {"subagent_stop", "sidekick_stop"} and e["payload"].get("final_message")
|
||||
]
|
||||
tools = [
|
||||
e["payload"] for e in events if e["kind"] in {"tool_result", "tool_failure"}
|
||||
@@ -1597,7 +1595,7 @@ def build_episode(
|
||||
}
|
||||
)
|
||||
pending_user_messages = []
|
||||
elif event["kind"] == "sidekick_stop":
|
||||
elif event["kind"] in {"subagent_stop", "sidekick_stop"}:
|
||||
pass
|
||||
extraction_messages.extend(pending_user_messages)
|
||||
|
||||
@@ -1613,7 +1611,7 @@ def build_episode(
|
||||
"assistant_conclusion": conclusion,
|
||||
"user_messages": prompts,
|
||||
"assistant_outcomes": assistant_conclusions,
|
||||
"sidekick_outcomes": sidekick_outcomes,
|
||||
"subagent_outcomes": subagent_outcomes,
|
||||
"extraction_messages": extraction_messages,
|
||||
"files_read": read_paths[:50],
|
||||
"files_modified": modified_paths[:50],
|
||||
|
||||
@@ -803,7 +803,7 @@ def test_tool_capture_identifies_main_and_sidekick_roles():
|
||||
)
|
||||
|
||||
assert main["agent_role"] == "main"
|
||||
assert sidekick["agent_role"] == "sidekick"
|
||||
assert sidekick["agent_role"] == "subagent"
|
||||
assert sidekick["agent_id"] == "agent-123"
|
||||
assert sidekick["agent_type"] == "mem0:sidekick"
|
||||
|
||||
@@ -1665,7 +1665,7 @@ def test_checkpoint_queues_only_prod_extraction_with_canonical_evidence(
|
||||
store.record_event(
|
||||
repo(),
|
||||
"s1",
|
||||
"sidekick_stop",
|
||||
"subagent_stop",
|
||||
{
|
||||
"agent_id": "agent-1",
|
||||
"agent_type": "mem0:sidekick",
|
||||
@@ -2337,9 +2337,9 @@ def test_sidekick_reuses_parent_memory_once_and_records_lifecycle(
|
||||
}
|
||||
|
||||
with patch.object(memory_core, "resolve_repo", return_value=repo()):
|
||||
first = memory_core.record_sidekick_start(store, start_input)
|
||||
repeated = memory_core.record_sidekick_start(store, start_input)
|
||||
memory_core.record_sidekick_stop(
|
||||
first = memory_core.record_subagent_start(store, start_input)
|
||||
repeated = memory_core.record_subagent_start(store, start_input)
|
||||
memory_core.record_subagent_stop(
|
||||
store,
|
||||
{
|
||||
**start_input,
|
||||
@@ -2359,11 +2359,11 @@ def test_sidekick_reuses_parent_memory_once_and_records_lifecycle(
|
||||
row[0]
|
||||
for row in store.conn.execute("SELECT kind FROM events ORDER BY id").fetchall()
|
||||
]
|
||||
assert events == ["sidekick_start", "sidekick_start", "sidekick_stop"]
|
||||
assert events == ["subagent_start", "subagent_start", "subagent_stop"]
|
||||
store.close()
|
||||
|
||||
|
||||
def test_sidekick_stop_without_agent_id_closes_latest_matching_run(isolated_env):
|
||||
def test_subagent_stop_without_agent_id_closes_latest_matching_run(isolated_env):
|
||||
store = memory_core.EvidenceStore()
|
||||
start_input = {
|
||||
"session_id": "s1",
|
||||
@@ -2373,8 +2373,8 @@ def test_sidekick_stop_without_agent_id_closes_latest_matching_run(isolated_env)
|
||||
}
|
||||
|
||||
with patch.object(memory_core, "resolve_repo", return_value=repo()):
|
||||
memory_core.record_sidekick_start(store, start_input)
|
||||
memory_core.record_sidekick_stop(
|
||||
memory_core.record_subagent_start(store, start_input)
|
||||
memory_core.record_subagent_stop(
|
||||
store,
|
||||
{
|
||||
"session_id": "s1",
|
||||
@@ -2836,14 +2836,14 @@ def test_status_output_explains_memory_activity_in_plain_language(capsys):
|
||||
"events": 8,
|
||||
"flushes": 2,
|
||||
"retrievals": 3,
|
||||
"sidekick_runs": 1,
|
||||
"subagent_runs": 1,
|
||||
"last_operation": {
|
||||
"operation": "flush",
|
||||
"success": 1,
|
||||
"duration_ms": 123.4,
|
||||
"item_count": 2,
|
||||
},
|
||||
"last_sidekick": {
|
||||
"last_subagent": {
|
||||
"stopped_at": "now",
|
||||
"context_chars": 240,
|
||||
"agent_id": "agent-1",
|
||||
@@ -2853,9 +2853,9 @@ def test_status_output_explains_memory_activity_in_plain_language(capsys):
|
||||
|
||||
output = capsys.readouterr().out
|
||||
assert "Saved on this computer: 8 session details, 2 memory updates" in output
|
||||
assert "3 memories returned, 1 sidekick runs" in output
|
||||
assert "3 memories returned, 1 subagent runs" in output
|
||||
assert "Last memory update: succeeded" in output
|
||||
assert "Last sidekick: finished, received 240 characters of memory" in output
|
||||
assert "Last subagent: finished, received 240 characters of memory" in output
|
||||
assert "checkpoint" not in output.lower()
|
||||
assert "injected" not in output.lower()
|
||||
|
||||
@@ -3391,7 +3391,7 @@ def test_plugin_entrypoints_share_explicit_claude_data_dir(tmp_path, monkeypatch
|
||||
status_payload = json.loads(status.stdout)
|
||||
assert status_payload["data_dir"] == str(canonical_data)
|
||||
assert status_payload["retrievals"] == 1
|
||||
assert status_payload["sidekick_runs"] == 1
|
||||
assert status_payload["subagent_runs"] == 1
|
||||
assert not (conflicting_data / "evidence.sqlite3").exists()
|
||||
|
||||
|
||||
@@ -4593,9 +4593,9 @@ def test_extraction_batches_bound_individual_messages_without_losing_text():
|
||||
|
||||
def test_overlapping_subagent_stop_without_id_does_not_guess(isolated_env):
|
||||
store = memory_core.EvidenceStore()
|
||||
store.start_sidekick(repo(), "s1", "first", "sidekick", 0)
|
||||
store.start_sidekick(repo(), "s1", "second", "sidekick", 0)
|
||||
stopped = store.stop_sidekick(repo(), "s1", "", "sidekick", "", "Uncorrelated response")
|
||||
store.start_subagent(repo(), "s1", "first", "sidekick", 0)
|
||||
store.start_subagent(repo(), "s1", "second", "sidekick", 0)
|
||||
stopped = store.stop_subagent(repo(), "s1", "", "sidekick", "", "Uncorrelated response")
|
||||
assert stopped not in {"first", "second"}
|
||||
rows = store.conn.execute("SELECT agent_id, stopped_at, final_message FROM sidekick_runs").fetchall()
|
||||
assert all(row["stopped_at"] is None for row in rows if row["agent_id"] in {"first", "second"})
|
||||
|
||||
@@ -11,10 +11,10 @@ import telemetry
|
||||
from memory_core import (
|
||||
EvidenceStore,
|
||||
api_key,
|
||||
configure_harness,
|
||||
data_dir,
|
||||
doctor,
|
||||
forget_remote_repo,
|
||||
configure_harness,
|
||||
resolve_repo,
|
||||
user_id,
|
||||
)
|
||||
@@ -32,7 +32,7 @@ def _print_status(value: dict) -> None:
|
||||
)
|
||||
print(
|
||||
f"Used in this repository: {value['retrievals']} memories returned, "
|
||||
f"{value['sidekick_runs']} sidekick runs"
|
||||
f"{value['subagent_runs']} subagent runs"
|
||||
)
|
||||
if last:
|
||||
item_label = ""
|
||||
@@ -48,13 +48,13 @@ def _print_status(value: dict) -> None:
|
||||
f"{'succeeded' if last['success'] else 'failed'} "
|
||||
f"({last['duration_ms']:.1f} ms{item_label})"
|
||||
)
|
||||
sidekick = value.get("last_sidekick") or {}
|
||||
if sidekick:
|
||||
state = "finished" if sidekick.get("stopped_at") else "started"
|
||||
subagent = value.get("last_subagent") or {}
|
||||
if subagent:
|
||||
state = "finished" if subagent.get("stopped_at") else "started"
|
||||
print(
|
||||
"Last sidekick: "
|
||||
f"{state}, received {sidekick['context_chars']} characters of memory, "
|
||||
f"agent {sidekick['agent_id']}"
|
||||
"Last subagent: "
|
||||
f"{state}, received {subagent['context_chars']} characters of memory, "
|
||||
f"agent {subagent['agent_id']}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -524,7 +524,7 @@ def _checkpoint_message(event: dict[str, Any]) -> str:
|
||||
if text:
|
||||
return text
|
||||
return redact(payload.get("text", "")).strip()
|
||||
if kind == "sidekick_stop":
|
||||
if kind in {"subagent_stop", "sidekick_stop"}:
|
||||
return redact(payload.get("final_message", "")).strip()
|
||||
return ""
|
||||
|
||||
@@ -596,6 +596,7 @@ class EvidenceStore:
|
||||
self.conn.close()
|
||||
|
||||
def _migrate(self) -> None:
|
||||
# Keep the legacy table name so existing databases and in-flight workers remain compatible.
|
||||
self.conn.executescript(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
@@ -688,8 +689,7 @@ class EvidenceStore:
|
||||
|
||||
"""
|
||||
)
|
||||
# Remove the pre-0.1.1 no-tools snapshot implementation. The real coding
|
||||
# sidekick is a native Claude Code agent and stores no state in this DB.
|
||||
# Remove the pre-0.1.1 snapshot implementation.
|
||||
self.conn.executescript(
|
||||
"""
|
||||
DROP TABLE IF EXISTS sidekick_calls;
|
||||
@@ -1041,7 +1041,7 @@ class EvidenceStore:
|
||||
if row["memory_text"]
|
||||
]
|
||||
|
||||
def start_sidekick(
|
||||
def start_subagent(
|
||||
self,
|
||||
repo: RepoContext,
|
||||
session_id: str,
|
||||
@@ -1049,7 +1049,7 @@ class EvidenceStore:
|
||||
agent_type: str,
|
||||
context_chars: int,
|
||||
) -> bool:
|
||||
"""Record one native sidekick instance and whether context was first sent."""
|
||||
"""Record one native subagent instance and whether context was first sent."""
|
||||
with self.conn:
|
||||
cursor = self.conn.execute(
|
||||
"""INSERT OR IGNORE INTO sidekick_runs
|
||||
@@ -1067,7 +1067,7 @@ class EvidenceStore:
|
||||
)
|
||||
return int(cursor.rowcount) > 0
|
||||
|
||||
def stop_sidekick(
|
||||
def stop_subagent(
|
||||
self,
|
||||
repo: RepoContext,
|
||||
session_id: str,
|
||||
@@ -1222,7 +1222,7 @@ class EvidenceStore:
|
||||
cursor = self.conn.execute(
|
||||
f"DELETE FROM {table} WHERE {column} = ?", (repo_id,)
|
||||
)
|
||||
removed[table] = max(int(cursor.rowcount), 0)
|
||||
removed["subagent_runs" if table == "sidekick_runs" else table] = max(int(cursor.rowcount), 0)
|
||||
return removed
|
||||
|
||||
def status(self, repo_id: str) -> dict[str, Any]:
|
||||
@@ -1238,7 +1238,7 @@ class EvidenceStore:
|
||||
FROM operations WHERE repo_id = ? ORDER BY id DESC LIMIT 1""",
|
||||
(repo_id,),
|
||||
).fetchone()
|
||||
last_sidekick = self.conn.execute(
|
||||
last_subagent = self.conn.execute(
|
||||
"""SELECT session_id, agent_id, agent_type, started_at, stopped_at,
|
||||
context_chars
|
||||
FROM sidekick_runs WHERE repo_id = ?
|
||||
@@ -1250,9 +1250,9 @@ class EvidenceStore:
|
||||
"events": count("events"),
|
||||
"flushes": count("flushes"),
|
||||
"retrievals": count("retrievals"),
|
||||
"sidekick_runs": count("sidekick_runs"),
|
||||
"subagent_runs": count("sidekick_runs"),
|
||||
"last_operation": dict(last_operation) if last_operation else None,
|
||||
"last_sidekick": dict(last_sidekick) if last_sidekick else None,
|
||||
"last_subagent": dict(last_subagent) if last_subagent else None,
|
||||
}
|
||||
|
||||
|
||||
@@ -1324,7 +1324,7 @@ def tool_payload(hook_input: dict[str, Any], *, failed: bool | None = False) ->
|
||||
"tool": name,
|
||||
"failed": failed,
|
||||
"duration_ms": hook_input.get("duration_ms"),
|
||||
"agent_role": "sidekick" if hook_input.get("agent_id") else "main",
|
||||
"agent_role": "subagent" if hook_input.get("agent_id") else "main",
|
||||
}
|
||||
if hook_input.get("agent_id"):
|
||||
payload["agent_id"] = bounded(hook_input["agent_id"], 200)
|
||||
@@ -1384,35 +1384,33 @@ def record_tool(
|
||||
)
|
||||
|
||||
|
||||
def record_sidekick_start(
|
||||
store: EvidenceStore, hook_input: dict[str, Any], *, inject_context: bool = True
|
||||
def record_subagent_start(
|
||||
store: EvidenceStore, hook_input: dict[str, Any]
|
||||
) -> str:
|
||||
"""Record a native sidekick and reuse the main turn's retrieved memories."""
|
||||
"""Record a native subagent and reuse the main turn's retrieved memories."""
|
||||
session_id = _session_id(hook_input)
|
||||
repo = store.repo_for_session(session_id, hook_input.get("cwd"))
|
||||
agent_id = bounded(hook_input.get("agent_id", "unknown-agent"), 200)
|
||||
agent_type = bounded(hook_input.get("agent_type", "mem0:sidekick"), 200)
|
||||
agent_type = bounded(hook_input.get("agent_type", "unknown-agent"), 200)
|
||||
context = combine_context(
|
||||
format_context(store.injected_memories(session_id, repo.identity))
|
||||
)
|
||||
if not inject_context:
|
||||
context = ""
|
||||
first_start = store.start_sidekick(
|
||||
first_start = store.start_subagent(
|
||||
repo, session_id, agent_id, agent_type, len(context)
|
||||
)
|
||||
store.record_event(
|
||||
repo,
|
||||
session_id,
|
||||
"sidekick_start",
|
||||
"subagent_start",
|
||||
{
|
||||
"agent_id": agent_id,
|
||||
"agent_type": agent_type,
|
||||
"context_chars": len(context) if first_start else 0,
|
||||
"worktree_root": bounded(repo.root, 2000),
|
||||
"repo_root": bounded(repo.root, 2000),
|
||||
},
|
||||
)
|
||||
telemetry.record(
|
||||
"sidekick",
|
||||
"subagent",
|
||||
repo=repo,
|
||||
session_id=session_id,
|
||||
phase="start",
|
||||
@@ -1422,14 +1420,14 @@ def record_sidekick_start(
|
||||
return context if first_start else ""
|
||||
|
||||
|
||||
def record_sidekick_stop(store: EvidenceStore, hook_input: dict[str, Any]) -> None:
|
||||
def record_subagent_stop(store: EvidenceStore, hook_input: dict[str, Any]) -> None:
|
||||
session_id = _session_id(hook_input)
|
||||
repo = store.repo_for_session(session_id, hook_input.get("cwd"))
|
||||
agent_type = bounded(hook_input.get("agent_type", "mem0:sidekick"), 200)
|
||||
agent_type = bounded(hook_input.get("agent_type", "unknown-agent"), 200)
|
||||
agent_id = bounded(hook_input.get("agent_id", ""), 200)
|
||||
final_message = redact(hook_input.get("last_assistant_message", "")).strip()
|
||||
transcript_path = bounded(hook_input.get("agent_transcript_path", ""), 2000)
|
||||
agent_id = store.stop_sidekick(
|
||||
agent_id = store.stop_subagent(
|
||||
repo,
|
||||
session_id,
|
||||
agent_id,
|
||||
@@ -1440,7 +1438,7 @@ def record_sidekick_stop(store: EvidenceStore, hook_input: dict[str, Any]) -> No
|
||||
store.record_event(
|
||||
repo,
|
||||
session_id,
|
||||
"sidekick_stop",
|
||||
"subagent_stop",
|
||||
{
|
||||
"agent_id": agent_id,
|
||||
"agent_type": agent_type,
|
||||
@@ -1449,7 +1447,7 @@ def record_sidekick_stop(store: EvidenceStore, hook_input: dict[str, Any]) -> No
|
||||
},
|
||||
)
|
||||
telemetry.record(
|
||||
"sidekick",
|
||||
"subagent",
|
||||
repo=repo,
|
||||
session_id=session_id,
|
||||
phase="stop",
|
||||
@@ -1513,10 +1511,10 @@ def build_episode(
|
||||
for e in events
|
||||
if e["kind"] == "assistant_stop" and e["payload"].get("text")
|
||||
]
|
||||
sidekick_outcomes = [
|
||||
subagent_outcomes = [
|
||||
redact(e["payload"].get("final_message", "")).strip()
|
||||
for e in events
|
||||
if e["kind"] == "sidekick_stop" and e["payload"].get("final_message")
|
||||
if e["kind"] in {"subagent_stop", "sidekick_stop"} and e["payload"].get("final_message")
|
||||
]
|
||||
tools = [
|
||||
e["payload"] for e in events if e["kind"] in {"tool_result", "tool_failure"}
|
||||
@@ -1597,7 +1595,7 @@ def build_episode(
|
||||
}
|
||||
)
|
||||
pending_user_messages = []
|
||||
elif event["kind"] == "sidekick_stop":
|
||||
elif event["kind"] in {"subagent_stop", "sidekick_stop"}:
|
||||
pass
|
||||
extraction_messages.extend(pending_user_messages)
|
||||
|
||||
@@ -1613,7 +1611,7 @@ def build_episode(
|
||||
"assistant_conclusion": conclusion,
|
||||
"user_messages": prompts,
|
||||
"assistant_outcomes": assistant_conclusions,
|
||||
"sidekick_outcomes": sidekick_outcomes,
|
||||
"subagent_outcomes": subagent_outcomes,
|
||||
"extraction_messages": extraction_messages,
|
||||
"files_read": read_paths[:50],
|
||||
"files_modified": modified_paths[:50],
|
||||
|
||||
@@ -15,8 +15,8 @@ import hook_runner # noqa: E402
|
||||
import telemetry # noqa: E402
|
||||
from memory_core import ( # noqa: E402
|
||||
configure_harness,
|
||||
record_sidekick_start,
|
||||
record_sidekick_stop,
|
||||
record_subagent_start,
|
||||
record_subagent_stop,
|
||||
record_tool,
|
||||
)
|
||||
|
||||
@@ -24,8 +24,8 @@ configure_harness("codex", data_dir_name="codex-plugin", source_tag="codex_plugi
|
||||
telemetry.init(harness="codex", source_tag="CODEX_PLUGIN")
|
||||
|
||||
|
||||
def _sidekick_start(store, hook_input):
|
||||
context = record_sidekick_start(store, hook_input)
|
||||
def _subagent_start(store, hook_input):
|
||||
context = record_subagent_start(store, hook_input)
|
||||
if context:
|
||||
return {
|
||||
"hookSpecificOutput": {
|
||||
@@ -35,8 +35,8 @@ def _sidekick_start(store, hook_input):
|
||||
}
|
||||
|
||||
|
||||
def _sidekick_stop(store, hook_input):
|
||||
record_sidekick_stop(store, hook_input)
|
||||
def _subagent_stop(store, hook_input):
|
||||
record_subagent_stop(store, hook_input)
|
||||
|
||||
|
||||
def _post_tool(store, payload):
|
||||
@@ -59,6 +59,6 @@ if __name__ == "__main__":
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "post-tool":
|
||||
sys.argv[1] = "codex-post-tool"
|
||||
hook_runner.entry_point(
|
||||
extra_actions={"sidekick-start": _sidekick_start, "sidekick-stop": _sidekick_stop, "codex-post-tool": _post_tool},
|
||||
extra_actions={"subagent-start": _subagent_start, "subagent-stop": _subagent_stop, "codex-post-tool": _post_tool},
|
||||
automatic_flush_reasons={"session-end", "pre-compact"},
|
||||
)
|
||||
|
||||
@@ -27,14 +27,14 @@
|
||||
"SubagentStart": [
|
||||
{
|
||||
"hooks": [
|
||||
{ "type": "command", "command": "python3 \"${PLUGIN_ROOT}/hooks/adapter.py\" sidekick-start --plugin-data-dir \"${PLUGIN_DATA}\"", "timeout": 3 }
|
||||
{ "type": "command", "command": "python3 \"${PLUGIN_ROOT}/hooks/adapter.py\" subagent-start --plugin-data-dir \"${PLUGIN_DATA}\"", "timeout": 3 }
|
||||
]
|
||||
}
|
||||
],
|
||||
"SubagentStop": [
|
||||
{
|
||||
"hooks": [
|
||||
{ "type": "command", "command": "python3 \"${PLUGIN_ROOT}/hooks/adapter.py\" sidekick-stop --plugin-data-dir \"${PLUGIN_DATA}\"", "timeout": 3 }
|
||||
{ "type": "command", "command": "python3 \"${PLUGIN_ROOT}/hooks/adapter.py\" subagent-stop --plugin-data-dir \"${PLUGIN_DATA}\"", "timeout": 3 }
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
@@ -35,7 +35,7 @@ def test_codex_hooks_use_native_events_and_plugin_paths() -> None:
|
||||
assert all(hook.get("timeout", 0) <= 3 for groups in hooks.values() for group in groups for hook in group["hooks"])
|
||||
|
||||
|
||||
def test_codex_sidekick_records_lifecycle(tmp_path: Path) -> None:
|
||||
def test_codex_subagent_records_lifecycle(tmp_path: Path) -> None:
|
||||
adapter = HOST / "hooks" / "adapter.py"
|
||||
payload = {
|
||||
"session_id": "session-1",
|
||||
@@ -45,8 +45,8 @@ def test_codex_sidekick_records_lifecycle(tmp_path: Path) -> None:
|
||||
}
|
||||
|
||||
for action, extra in (
|
||||
("sidekick-start", {}),
|
||||
("sidekick-stop", {"last_assistant_message": "SIDEKICK_OK"}),
|
||||
("subagent-start", {}),
|
||||
("subagent-stop", {"last_assistant_message": "SUBAGENT_OK"}),
|
||||
):
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(adapter), action, "--plugin-data-dir", str(tmp_path / "data")],
|
||||
@@ -65,7 +65,7 @@ def test_codex_sidekick_records_lifecycle(tmp_path: Path) -> None:
|
||||
assert row is not None
|
||||
assert row[0] == "agent-1"
|
||||
assert row[1]
|
||||
assert row[2] == "SIDEKICK_OK"
|
||||
assert row[2] == "SUBAGENT_OK"
|
||||
|
||||
|
||||
def test_codex_mcp_uses_host_relative_paths() -> None:
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"license": "Apache-2.0",
|
||||
"keywords": ["memory", "coding-agents", "continual-learning", "token-efficiency"],
|
||||
"skills": "./skills/",
|
||||
"agents": "./agents/",
|
||||
"hooks": "./hooks/hooks.json",
|
||||
"mcpServers": "./mcp.json",
|
||||
"variables": {
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
name: sidekick
|
||||
description: Coding agent with an isolated context. Delegate focused investigation, implementation, testing, debugging, or review work to it, then review its result.
|
||||
model: inherit
|
||||
---
|
||||
|
||||
You are Mem0's Cursor sidekick. Complete only the work the parent agent assigns.
|
||||
Return a tested result the parent can review without repeating your investigation.
|
||||
|
||||
Always call `search_memories` before work that can depend on earlier decisions,
|
||||
repository history, or user preferences. Inspect the relevant code and repository
|
||||
rules, make requested edits, and run the smallest decisive checks.
|
||||
|
||||
Do not make adjacent improvements. Ask the parent one concise question only when
|
||||
a material decision or unsafe ambiguity blocks progress. Otherwise proceed and
|
||||
report the outcome, changed files, validation, and remaining risk.
|
||||
@@ -11,10 +11,10 @@ import telemetry
|
||||
from memory_core import (
|
||||
EvidenceStore,
|
||||
api_key,
|
||||
configure_harness,
|
||||
data_dir,
|
||||
doctor,
|
||||
forget_remote_repo,
|
||||
configure_harness,
|
||||
resolve_repo,
|
||||
user_id,
|
||||
)
|
||||
@@ -32,7 +32,7 @@ def _print_status(value: dict) -> None:
|
||||
)
|
||||
print(
|
||||
f"Used in this repository: {value['retrievals']} memories returned, "
|
||||
f"{value['sidekick_runs']} sidekick runs"
|
||||
f"{value['subagent_runs']} subagent runs"
|
||||
)
|
||||
if last:
|
||||
item_label = ""
|
||||
@@ -48,13 +48,13 @@ def _print_status(value: dict) -> None:
|
||||
f"{'succeeded' if last['success'] else 'failed'} "
|
||||
f"({last['duration_ms']:.1f} ms{item_label})"
|
||||
)
|
||||
sidekick = value.get("last_sidekick") or {}
|
||||
if sidekick:
|
||||
state = "finished" if sidekick.get("stopped_at") else "started"
|
||||
subagent = value.get("last_subagent") or {}
|
||||
if subagent:
|
||||
state = "finished" if subagent.get("stopped_at") else "started"
|
||||
print(
|
||||
"Last sidekick: "
|
||||
f"{state}, received {sidekick['context_chars']} characters of memory, "
|
||||
f"agent {sidekick['agent_id']}"
|
||||
"Last subagent: "
|
||||
f"{state}, received {subagent['context_chars']} characters of memory, "
|
||||
f"agent {subagent['agent_id']}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -524,7 +524,7 @@ def _checkpoint_message(event: dict[str, Any]) -> str:
|
||||
if text:
|
||||
return text
|
||||
return redact(payload.get("text", "")).strip()
|
||||
if kind == "sidekick_stop":
|
||||
if kind in {"subagent_stop", "sidekick_stop"}:
|
||||
return redact(payload.get("final_message", "")).strip()
|
||||
return ""
|
||||
|
||||
@@ -596,6 +596,7 @@ class EvidenceStore:
|
||||
self.conn.close()
|
||||
|
||||
def _migrate(self) -> None:
|
||||
# Keep the legacy table name so existing databases and in-flight workers remain compatible.
|
||||
self.conn.executescript(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
@@ -688,8 +689,7 @@ class EvidenceStore:
|
||||
|
||||
"""
|
||||
)
|
||||
# Remove the pre-0.1.1 no-tools snapshot implementation. The real coding
|
||||
# sidekick is a native Claude Code agent and stores no state in this DB.
|
||||
# Remove the pre-0.1.1 snapshot implementation.
|
||||
self.conn.executescript(
|
||||
"""
|
||||
DROP TABLE IF EXISTS sidekick_calls;
|
||||
@@ -1041,7 +1041,7 @@ class EvidenceStore:
|
||||
if row["memory_text"]
|
||||
]
|
||||
|
||||
def start_sidekick(
|
||||
def start_subagent(
|
||||
self,
|
||||
repo: RepoContext,
|
||||
session_id: str,
|
||||
@@ -1049,7 +1049,7 @@ class EvidenceStore:
|
||||
agent_type: str,
|
||||
context_chars: int,
|
||||
) -> bool:
|
||||
"""Record one native sidekick instance and whether context was first sent."""
|
||||
"""Record one native subagent instance and whether context was first sent."""
|
||||
with self.conn:
|
||||
cursor = self.conn.execute(
|
||||
"""INSERT OR IGNORE INTO sidekick_runs
|
||||
@@ -1067,7 +1067,7 @@ class EvidenceStore:
|
||||
)
|
||||
return int(cursor.rowcount) > 0
|
||||
|
||||
def stop_sidekick(
|
||||
def stop_subagent(
|
||||
self,
|
||||
repo: RepoContext,
|
||||
session_id: str,
|
||||
@@ -1222,7 +1222,7 @@ class EvidenceStore:
|
||||
cursor = self.conn.execute(
|
||||
f"DELETE FROM {table} WHERE {column} = ?", (repo_id,)
|
||||
)
|
||||
removed[table] = max(int(cursor.rowcount), 0)
|
||||
removed["subagent_runs" if table == "sidekick_runs" else table] = max(int(cursor.rowcount), 0)
|
||||
return removed
|
||||
|
||||
def status(self, repo_id: str) -> dict[str, Any]:
|
||||
@@ -1238,7 +1238,7 @@ class EvidenceStore:
|
||||
FROM operations WHERE repo_id = ? ORDER BY id DESC LIMIT 1""",
|
||||
(repo_id,),
|
||||
).fetchone()
|
||||
last_sidekick = self.conn.execute(
|
||||
last_subagent = self.conn.execute(
|
||||
"""SELECT session_id, agent_id, agent_type, started_at, stopped_at,
|
||||
context_chars
|
||||
FROM sidekick_runs WHERE repo_id = ?
|
||||
@@ -1250,9 +1250,9 @@ class EvidenceStore:
|
||||
"events": count("events"),
|
||||
"flushes": count("flushes"),
|
||||
"retrievals": count("retrievals"),
|
||||
"sidekick_runs": count("sidekick_runs"),
|
||||
"subagent_runs": count("sidekick_runs"),
|
||||
"last_operation": dict(last_operation) if last_operation else None,
|
||||
"last_sidekick": dict(last_sidekick) if last_sidekick else None,
|
||||
"last_subagent": dict(last_subagent) if last_subagent else None,
|
||||
}
|
||||
|
||||
|
||||
@@ -1324,7 +1324,7 @@ def tool_payload(hook_input: dict[str, Any], *, failed: bool | None = False) ->
|
||||
"tool": name,
|
||||
"failed": failed,
|
||||
"duration_ms": hook_input.get("duration_ms"),
|
||||
"agent_role": "sidekick" if hook_input.get("agent_id") else "main",
|
||||
"agent_role": "subagent" if hook_input.get("agent_id") else "main",
|
||||
}
|
||||
if hook_input.get("agent_id"):
|
||||
payload["agent_id"] = bounded(hook_input["agent_id"], 200)
|
||||
@@ -1384,35 +1384,33 @@ def record_tool(
|
||||
)
|
||||
|
||||
|
||||
def record_sidekick_start(
|
||||
store: EvidenceStore, hook_input: dict[str, Any], *, inject_context: bool = True
|
||||
def record_subagent_start(
|
||||
store: EvidenceStore, hook_input: dict[str, Any]
|
||||
) -> str:
|
||||
"""Record a native sidekick and reuse the main turn's retrieved memories."""
|
||||
"""Record a native subagent and reuse the main turn's retrieved memories."""
|
||||
session_id = _session_id(hook_input)
|
||||
repo = store.repo_for_session(session_id, hook_input.get("cwd"))
|
||||
agent_id = bounded(hook_input.get("agent_id", "unknown-agent"), 200)
|
||||
agent_type = bounded(hook_input.get("agent_type", "mem0:sidekick"), 200)
|
||||
agent_type = bounded(hook_input.get("agent_type", "unknown-agent"), 200)
|
||||
context = combine_context(
|
||||
format_context(store.injected_memories(session_id, repo.identity))
|
||||
)
|
||||
if not inject_context:
|
||||
context = ""
|
||||
first_start = store.start_sidekick(
|
||||
first_start = store.start_subagent(
|
||||
repo, session_id, agent_id, agent_type, len(context)
|
||||
)
|
||||
store.record_event(
|
||||
repo,
|
||||
session_id,
|
||||
"sidekick_start",
|
||||
"subagent_start",
|
||||
{
|
||||
"agent_id": agent_id,
|
||||
"agent_type": agent_type,
|
||||
"context_chars": len(context) if first_start else 0,
|
||||
"worktree_root": bounded(repo.root, 2000),
|
||||
"repo_root": bounded(repo.root, 2000),
|
||||
},
|
||||
)
|
||||
telemetry.record(
|
||||
"sidekick",
|
||||
"subagent",
|
||||
repo=repo,
|
||||
session_id=session_id,
|
||||
phase="start",
|
||||
@@ -1422,14 +1420,14 @@ def record_sidekick_start(
|
||||
return context if first_start else ""
|
||||
|
||||
|
||||
def record_sidekick_stop(store: EvidenceStore, hook_input: dict[str, Any]) -> None:
|
||||
def record_subagent_stop(store: EvidenceStore, hook_input: dict[str, Any]) -> None:
|
||||
session_id = _session_id(hook_input)
|
||||
repo = store.repo_for_session(session_id, hook_input.get("cwd"))
|
||||
agent_type = bounded(hook_input.get("agent_type", "mem0:sidekick"), 200)
|
||||
agent_type = bounded(hook_input.get("agent_type", "unknown-agent"), 200)
|
||||
agent_id = bounded(hook_input.get("agent_id", ""), 200)
|
||||
final_message = redact(hook_input.get("last_assistant_message", "")).strip()
|
||||
transcript_path = bounded(hook_input.get("agent_transcript_path", ""), 2000)
|
||||
agent_id = store.stop_sidekick(
|
||||
agent_id = store.stop_subagent(
|
||||
repo,
|
||||
session_id,
|
||||
agent_id,
|
||||
@@ -1440,7 +1438,7 @@ def record_sidekick_stop(store: EvidenceStore, hook_input: dict[str, Any]) -> No
|
||||
store.record_event(
|
||||
repo,
|
||||
session_id,
|
||||
"sidekick_stop",
|
||||
"subagent_stop",
|
||||
{
|
||||
"agent_id": agent_id,
|
||||
"agent_type": agent_type,
|
||||
@@ -1449,7 +1447,7 @@ def record_sidekick_stop(store: EvidenceStore, hook_input: dict[str, Any]) -> No
|
||||
},
|
||||
)
|
||||
telemetry.record(
|
||||
"sidekick",
|
||||
"subagent",
|
||||
repo=repo,
|
||||
session_id=session_id,
|
||||
phase="stop",
|
||||
@@ -1513,10 +1511,10 @@ def build_episode(
|
||||
for e in events
|
||||
if e["kind"] == "assistant_stop" and e["payload"].get("text")
|
||||
]
|
||||
sidekick_outcomes = [
|
||||
subagent_outcomes = [
|
||||
redact(e["payload"].get("final_message", "")).strip()
|
||||
for e in events
|
||||
if e["kind"] == "sidekick_stop" and e["payload"].get("final_message")
|
||||
if e["kind"] in {"subagent_stop", "sidekick_stop"} and e["payload"].get("final_message")
|
||||
]
|
||||
tools = [
|
||||
e["payload"] for e in events if e["kind"] in {"tool_result", "tool_failure"}
|
||||
@@ -1597,7 +1595,7 @@ def build_episode(
|
||||
}
|
||||
)
|
||||
pending_user_messages = []
|
||||
elif event["kind"] == "sidekick_stop":
|
||||
elif event["kind"] in {"subagent_stop", "sidekick_stop"}:
|
||||
pass
|
||||
extraction_messages.extend(pending_user_messages)
|
||||
|
||||
@@ -1613,7 +1611,7 @@ def build_episode(
|
||||
"assistant_conclusion": conclusion,
|
||||
"user_messages": prompts,
|
||||
"assistant_outcomes": assistant_conclusions,
|
||||
"sidekick_outcomes": sidekick_outcomes,
|
||||
"subagent_outcomes": subagent_outcomes,
|
||||
"extraction_messages": extraction_messages,
|
||||
"files_read": read_paths[:50],
|
||||
"files_modified": modified_paths[:50],
|
||||
|
||||
@@ -19,8 +19,6 @@ import hook_runner # noqa: E402
|
||||
import telemetry # noqa: E402
|
||||
from memory_core import ( # noqa: E402
|
||||
configure_harness,
|
||||
record_sidekick_start,
|
||||
record_sidekick_stop,
|
||||
record_tool,
|
||||
)
|
||||
|
||||
@@ -30,8 +28,6 @@ EVENTS = {
|
||||
"postToolUse": "post-tool",
|
||||
"postToolUseFailure": "post-tool-failure",
|
||||
"afterAgentResponse": "assistant-stop",
|
||||
"subagentStart": "sidekick-start",
|
||||
"subagentStop": "sidekick-stop",
|
||||
"stop": "stop",
|
||||
"sessionEnd": "session-end",
|
||||
"preCompact": "pre-compact",
|
||||
@@ -52,10 +48,6 @@ def normalize(payload: dict, event: str) -> dict:
|
||||
value.setdefault("last_assistant_message", value["text"])
|
||||
if "summary" in value:
|
||||
value.setdefault("last_assistant_message", value["summary"])
|
||||
if "subagent_id" in value:
|
||||
value.setdefault("agent_id", value["subagent_id"])
|
||||
if "subagent_type" in value:
|
||||
value.setdefault("agent_type", value["subagent_type"])
|
||||
return {"action": EVENTS[event], "payload": value}
|
||||
|
||||
|
||||
@@ -67,15 +59,6 @@ def _record_response(store, payload):
|
||||
hook_runner.default_record_stop(store, payload)
|
||||
|
||||
|
||||
def _record_sidekick_start(store, payload):
|
||||
record_sidekick_start(store, payload, inject_context=False)
|
||||
return {"permission": "allow"}
|
||||
|
||||
|
||||
def _record_sidekick_stop(store, payload):
|
||||
record_sidekick_stop(store, payload)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) != 2 or sys.argv[1] not in EVENTS:
|
||||
return 2
|
||||
@@ -100,8 +83,6 @@ def main() -> int:
|
||||
extra_actions={
|
||||
"post-tool-failure": _record_failure,
|
||||
"assistant-stop": _record_response,
|
||||
"sidekick-start": _record_sidekick_start,
|
||||
"sidekick-stop": _record_sidekick_stop,
|
||||
},
|
||||
automatic_flush_reasons={"session-end", "pre-compact"},
|
||||
)
|
||||
@@ -119,8 +100,6 @@ def main() -> int:
|
||||
}
|
||||
if environment:
|
||||
print(json.dumps({"env": environment}))
|
||||
elif event == "subagentStart":
|
||||
print(json.dumps({"permission": "allow"}))
|
||||
return result
|
||||
|
||||
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
"postToolUse": [{ "command": "python3 \"${CURSOR_PLUGIN_ROOT}/hooks/adapter.py\" postToolUse", "timeout": 3 }],
|
||||
"postToolUseFailure": [{ "command": "python3 \"${CURSOR_PLUGIN_ROOT}/hooks/adapter.py\" postToolUseFailure", "timeout": 3 }],
|
||||
"afterAgentResponse": [{ "command": "python3 \"${CURSOR_PLUGIN_ROOT}/hooks/adapter.py\" afterAgentResponse", "timeout": 3 }],
|
||||
"subagentStart": [{ "command": "python3 \"${CURSOR_PLUGIN_ROOT}/hooks/adapter.py\" subagentStart", "matcher": "^sidekick$", "timeout": 5 }],
|
||||
"subagentStop": [{ "command": "python3 \"${CURSOR_PLUGIN_ROOT}/hooks/adapter.py\" subagentStop", "matcher": "^sidekick$", "timeout": 5 }],
|
||||
"stop": [{ "command": "python3 \"${CURSOR_PLUGIN_ROOT}/hooks/adapter.py\" stop", "timeout": 3 }],
|
||||
"preCompact": [{ "command": "python3 \"${CURSOR_PLUGIN_ROOT}/hooks/adapter.py\" preCompact", "timeout": 5 }],
|
||||
"sessionEnd": [{ "command": "python3 \"${CURSOR_PLUGIN_ROOT}/hooks/adapter.py\" sessionEnd", "timeout": 5 }]
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
".cursor-plugin/plugin.json": ".cursor-plugin/plugin.json",
|
||||
"hooks/hooks.json": "hooks/hooks.json",
|
||||
"hooks/adapter.py": "hooks/adapter.py",
|
||||
"agents/sidekick.md": "agents/sidekick.md",
|
||||
"mcp.json": "mcp.json"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,6 @@ SPEC.loader.exec_module(adapter)
|
||||
("postToolUse", "post-tool"),
|
||||
("postToolUseFailure", "post-tool-failure"),
|
||||
("afterAgentResponse", "assistant-stop"),
|
||||
("subagentStart", "sidekick-start"),
|
||||
("subagentStop", "sidekick-stop"),
|
||||
("stop", "stop"),
|
||||
("sessionEnd", "session-end"),
|
||||
("preCompact", "pre-compact"),
|
||||
@@ -57,48 +55,18 @@ def test_after_agent_response_does_not_return_internal_context(monkeypatch: pyte
|
||||
assert adapter._record_response(object(), {}) is None
|
||||
|
||||
|
||||
def test_normalizes_cursor_sidekick_fields() -> None:
|
||||
started = adapter.normalize(
|
||||
{
|
||||
"subagent_id": "agent-1",
|
||||
"subagent_type": "sidekick",
|
||||
"parent_conversation_id": "parent-1",
|
||||
},
|
||||
"subagentStart",
|
||||
)["payload"]
|
||||
stopped = adapter.normalize({"summary": "done"}, "subagentStop")["payload"]
|
||||
|
||||
assert started["agent_id"] == "agent-1"
|
||||
assert started["agent_type"] == "sidekick"
|
||||
assert started["session_id"] == "parent-1"
|
||||
assert stopped["last_assistant_message"] == "done"
|
||||
|
||||
|
||||
def test_cursor_sidekick_records_lifecycle_without_blocking(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(adapter, "record_sidekick_start", lambda store, payload, *, inject_context: "" if not inject_context else pytest.fail("cannot inject context"))
|
||||
stopped = []
|
||||
monkeypatch.setattr(adapter, "record_sidekick_stop", lambda store, payload: stopped.append(payload))
|
||||
|
||||
assert adapter._record_sidekick_start(object(), {}) == {"permission": "allow"}
|
||||
assert adapter._record_sidekick_stop(object(), {"summary": "done"}) is None
|
||||
assert stopped == [{"summary": "done"}]
|
||||
|
||||
|
||||
def test_cursor_hooks_use_native_flat_entries() -> None:
|
||||
hooks = json.loads((HOST / "hooks" / "hooks.json").read_text(encoding="utf-8"))
|
||||
|
||||
assert hooks["version"] == 1
|
||||
assert not {"subagentStart", "subagentStop"} & hooks["hooks"].keys()
|
||||
assert set(hooks["hooks"]) >= {
|
||||
"sessionStart",
|
||||
"beforeSubmitPrompt",
|
||||
"postToolUse",
|
||||
"subagentStart",
|
||||
"subagentStop",
|
||||
"stop",
|
||||
"sessionEnd",
|
||||
}
|
||||
assert hooks["hooks"]["subagentStart"][0]["matcher"] == "^sidekick$"
|
||||
assert hooks["hooks"]["subagentStop"][0]["matcher"] == "^sidekick$"
|
||||
assert all("hooks" not in entry for entries in hooks["hooks"].values() for entry in entries)
|
||||
|
||||
|
||||
@@ -107,6 +75,7 @@ def test_native_cursor_bundle_is_self_contained(tmp_path: Path) -> None:
|
||||
|
||||
manifest = json.loads((root / ".cursor-plugin" / "plugin.json").read_text(encoding="utf-8"))
|
||||
assert manifest["hooks"] == "./hooks/hooks.json"
|
||||
assert "agents" not in manifest
|
||||
assert (root / "hooks" / "adapter.py").is_file()
|
||||
assert (root / "agents" / "sidekick.md").is_file()
|
||||
assert not (root / "agents").exists()
|
||||
assert not any(path.is_symlink() for path in root.rglob("*"))
|
||||
|
||||
@@ -15,6 +15,8 @@ Unlike the local/file-based memory plugins in the ecosystem, Mem0 is a managed b
|
||||
|
||||
Current package version: `0.3.0`.
|
||||
|
||||
Sidekick is available only in the [Claude Code plugin](../claude-code-plugin/README.md#sonnet-sidekick-agent).
|
||||
|
||||
## How it works
|
||||
|
||||
A Cordis plugin is a module exporting `apply(ctx, config)`. This one waits for the Harness tool and system-prompt services, then uses the native extension points:
|
||||
@@ -61,7 +63,7 @@ Cordis owns listener and tool cleanup when the plugin unmounts. Every automatic
|
||||
```
|
||||
5. Open http://127.0.0.1:3080 and ask the agent to remember something, then recall it in a later turn.
|
||||
|
||||
For a Mem0 Platform on-prem or dedicated deployment, point `config.host` at that base URL (defaults to `api.mem0.ai`). `host` is a Platform base-URL override — it is not a switch to self-hosted Mem0 OSS, whose server exposes a different API surface.
|
||||
For a Mem0 Platform on-prem or dedicated deployment, point `config.host` at that base URL (defaults to `api.mem0.ai`). `host` overrides the Platform base URL. It does not support the self-hosted Mem0 OSS API.
|
||||
|
||||
## Configuration
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
name: sidekick
|
||||
description: Coding subagent for focused implementation, investigation, testing, debugging, or review work.
|
||||
whenToUse: Delegate a bounded engineering task that benefits from its own isolated context.
|
||||
---
|
||||
|
||||
You are Mem0's coding sidekick. Complete only the bounded task the main agent
|
||||
delegates to you and return a concise, self-contained result.
|
||||
|
||||
Search Mem0 before work that may depend on prior repository decisions or user
|
||||
preferences. Inspect the relevant repository rules and code, make changes when
|
||||
asked, and run the smallest decisive validation. Do not claim Git worktree
|
||||
isolation: Kimi provides a separate context, while filesystem isolation depends
|
||||
on the caller's environment.
|
||||
|
||||
Your final response must state the outcome, changed files, validation, and any
|
||||
remaining risk. Do not commit, push, or open a pull request unless the caller
|
||||
explicitly asks.
|
||||
@@ -11,10 +11,10 @@ import telemetry
|
||||
from memory_core import (
|
||||
EvidenceStore,
|
||||
api_key,
|
||||
configure_harness,
|
||||
data_dir,
|
||||
doctor,
|
||||
forget_remote_repo,
|
||||
configure_harness,
|
||||
resolve_repo,
|
||||
user_id,
|
||||
)
|
||||
@@ -32,7 +32,7 @@ def _print_status(value: dict) -> None:
|
||||
)
|
||||
print(
|
||||
f"Used in this repository: {value['retrievals']} memories returned, "
|
||||
f"{value['sidekick_runs']} sidekick runs"
|
||||
f"{value['subagent_runs']} subagent runs"
|
||||
)
|
||||
if last:
|
||||
item_label = ""
|
||||
@@ -48,13 +48,13 @@ def _print_status(value: dict) -> None:
|
||||
f"{'succeeded' if last['success'] else 'failed'} "
|
||||
f"({last['duration_ms']:.1f} ms{item_label})"
|
||||
)
|
||||
sidekick = value.get("last_sidekick") or {}
|
||||
if sidekick:
|
||||
state = "finished" if sidekick.get("stopped_at") else "started"
|
||||
subagent = value.get("last_subagent") or {}
|
||||
if subagent:
|
||||
state = "finished" if subagent.get("stopped_at") else "started"
|
||||
print(
|
||||
"Last sidekick: "
|
||||
f"{state}, received {sidekick['context_chars']} characters of memory, "
|
||||
f"agent {sidekick['agent_id']}"
|
||||
"Last subagent: "
|
||||
f"{state}, received {subagent['context_chars']} characters of memory, "
|
||||
f"agent {subagent['agent_id']}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user