Compare commits

..
Author SHA1 Message Date
harshgupta-mem0 d3d9cc9e26 fix(security): patch 8 HIGH + 18 MEDIUM Vanta vulnerabilities
Bumps three transitive packages across four pnpm workspaces via overrides.
Manifests and lockfiles only; no source changes.

  undici           7.28.0 -> 7.29.0   mem0-ts, openclaw, pi-agent-plugin
                   8.5.0  -> 8.10.0   pi-agent-plugin      (GHSA-4cwx-7wf7-3272 +4)
  ip-address       10.2.0 -> 10.4.0   mem0-ts              (GHSA-mwp4-54f8-5fhr +2)
  brace-expansion  2.1.2  -> 2.1.4    mem0-ts              (GHSA-rgw5-rvv9-x895,
                                                            GHSA-mh99-v99m-4gvg)
                   1.1.15 -> 1.1.18   zapier-mem0          (GHSA-3jxr-9vmj-r5cp)

Existing override keys were replaced in place rather than added alongside:
pnpm applies only the first override matching a bare package name, so a
stale broader key (e.g. undici@<6.27.0) would have shadowed a new narrower
one and silently held the vulnerable version.

Overrides are written to both package.json and pnpm-workspace.yaml because
openclaw, pi-agent-plugin and zapier-mem0 run pnpm 9 in CI, which reads
overrides only from package.json.

Verified: 26/41 open alerts clear against the regenerated lockfiles;
frozen-lockfile passes under each workspace's CI pnpm major; runtime API
smoke 7/7; mem0-ts 1505/1505, openclaw 446/446, pi-agent 100/100, zapier 17/17.

Fixes: https://app.vanta.com/c/mem0.ai/tests/packages-checked-for-vulnerabilities-v2-records-closed-github-dependabot-critical?tab=results
Fixes: https://app.vanta.com/c/mem0.ai/tests/packages-checked-for-vulnerabilities-v2-records-closed-github-dependabot-high?tab=results
2026-08-07 17:22:49 +05:30
Diwakar Ray YadavandGitHub b05dc2740f docs(memory): note filters-based scoping for search/get_all on add() (#6758) 2026-08-07 14:03:09 +05:30
KartikandGitHub 4a0a9a92a6 fix(ts-oss, py): release Oracle client on init failure, validate insert batches (#6839) 2026-08-06 18:16:25 +05:30
KartikandGitHub beea626f0a perf(ts-oss): cut Oracle vector store round trips per review feedback (#6835) 2026-08-06 15:12:14 +05:30
17 changed files with 331 additions and 106 deletions
+1 -1
View File
@@ -72,7 +72,7 @@
"@qdrant/js-client-rest": "^1.18.0",
"uuid@<11.1.1": ">=11.1.1",
"esbuild": ">=0.28.1",
"undici@<6.27.0": ">=6.27.0 <8.0.0",
"undici@<7.29.0": ">=7.29.0 <8.0.0",
"axios@<1.18.0": ">=1.18.0 <2.0.0",
"postcss@<8.5.18": ">=8.5.18 <9.0.0",
"mongoose@>=9.0.0 <9.7.2": ">=9.7.2 <10.0.0"
+5 -5
View File
@@ -13,7 +13,7 @@ overrides:
'@qdrant/js-client-rest': ^1.18.0
uuid@<11.1.1: '>=11.1.1'
esbuild: '>=0.28.1'
undici@<6.27.0: '>=6.27.0 <8.0.0'
undici@<7.29.0: '>=7.29.0 <8.0.0'
axios@<1.18.0: '>=1.18.0 <2.0.0'
postcss@<8.5.18: '>=8.5.18 <9.0.0'
mongoose@>=9.0.0 <9.7.2: '>=9.7.2 <10.0.0'
@@ -2036,8 +2036,8 @@ packages:
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
undici@7.28.0:
resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==}
undici@7.29.0:
resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==}
engines: {node: '>=20.18.1'}
util-deprecate@1.0.2:
@@ -2547,7 +2547,7 @@ snapshots:
dependencies:
'@qdrant/openapi-typescript-fetch': 1.2.6
typescript: 5.9.3
undici: 7.28.0
undici: 7.29.0
'@qdrant/openapi-typescript-fetch@1.2.6': {}
@@ -4121,7 +4121,7 @@ snapshots:
undici-types@6.21.0: {}
undici@7.28.0: {}
undici@7.29.0: {}
util-deprecate@1.0.2: {}
+1 -1
View File
@@ -21,7 +21,7 @@ overrides:
"@qdrant/js-client-rest": "^1.18.0"
"uuid@<11.1.1": ">=11.1.1"
"esbuild": ">=0.28.1"
"undici@<6.27.0": ">=6.27.0 <8.0.0"
"undici@<7.29.0": ">=7.29.0 <8.0.0"
"axios@<1.18.0": ">=1.18.0 <2.0.0"
"postcss@<8.5.18": ">=8.5.18 <9.0.0"
"mongoose@>=9.0.0 <9.7.2": ">=9.7.2 <10.0.0"
+2 -2
View File
@@ -73,8 +73,8 @@
"form-data@<4.0.6": ">=4.0.6",
"uuid@<11.1.1": ">=11.1.1",
"esbuild": ">=0.28.1",
"undici@<6.27.0": ">=6.27.0 <8.0.0",
"undici@>=8.0.0 <8.5.0": ">=8.5.0",
"undici@<7.29.0": ">=7.29.0 <8.0.0",
"undici@>=8.0.0 <8.9.0": ">=8.9.0 <9.0.0",
"axios@<1.18.0": ">=1.18.0 <2.0.0",
"brace-expansion@>=3.0.0 <5.0.8": ">=5.0.8 <6.0.0",
"postcss@<8.5.18": ">=8.5.18 <9.0.0",
+10 -10
View File
@@ -8,8 +8,8 @@ overrides:
form-data@<4.0.6: '>=4.0.6'
uuid@<11.1.1: '>=11.1.1'
esbuild: '>=0.28.1'
undici@<6.27.0: '>=6.27.0 <8.0.0'
undici@>=8.0.0 <8.5.0: '>=8.5.0'
undici@<7.29.0: '>=7.29.0 <8.0.0'
undici@>=8.0.0 <8.9.0: '>=8.9.0 <9.0.0'
axios@<1.18.0: '>=1.18.0 <2.0.0'
brace-expansion@>=3.0.0 <5.0.8: '>=5.0.8 <6.0.0'
postcss@<8.5.18: '>=8.5.18 <9.0.0'
@@ -2354,12 +2354,12 @@ packages:
undici-types@7.24.6:
resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
undici@7.28.0:
resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==}
undici@7.29.0:
resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==}
engines: {node: '>=20.18.1'}
undici@8.5.0:
resolution: {integrity: sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==}
undici@8.10.0:
resolution: {integrity: sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==}
engines: {node: '>=22.19.0'}
util-deprecate@1.0.2:
@@ -2942,7 +2942,7 @@ snapshots:
minimatch: 10.2.5
proper-lockfile: 4.1.2
typebox: 1.1.38
undici: 8.5.0
undici: 8.10.0
yaml: 2.9.0
optionalDependencies:
'@mariozechner/clipboard': 0.3.9
@@ -3203,7 +3203,7 @@ snapshots:
dependencies:
'@qdrant/openapi-typescript-fetch': 1.2.6
typescript: 6.0.3
undici: 7.28.0
undici: 7.29.0
'@qdrant/openapi-typescript-fetch@1.2.6': {}
@@ -4895,9 +4895,9 @@ snapshots:
undici-types@7.24.6: {}
undici@7.28.0: {}
undici@7.29.0: {}
undici@8.5.0: {}
undici@8.10.0: {}
util-deprecate@1.0.2: {}
@@ -5,8 +5,8 @@ overrides:
"form-data@<4.0.6": ">=4.0.6"
"uuid@<11.1.1": ">=11.1.1"
"esbuild": ">=0.28.1"
"undici@<6.27.0": ">=6.27.0 <8.0.0"
"undici@>=8.0.0 <8.5.0": ">=8.5.0"
"undici@<7.29.0": ">=7.29.0 <8.0.0"
"undici@>=8.0.0 <8.9.0": ">=8.9.0 <9.0.0"
"axios@<1.18.0": ">=1.18.0 <2.0.0"
"brace-expansion@>=3.0.0 <5.0.8": ">=5.0.8 <6.0.0"
"postcss@<8.5.18": ">=8.5.18 <9.0.0"
+13 -5
View File
@@ -41,10 +41,17 @@
],
"jest": {
"testEnvironment": "node",
"testMatch": ["**/test/**/*.test.ts"],
"setupFiles": ["<rootDir>/test/setup.ts"],
"testMatch": [
"**/test/**/*.test.ts"
],
"setupFiles": [
"<rootDir>/test/setup.ts"
],
"transform": {
"^.+\\.tsx?$": ["ts-jest", {}]
"^.+\\.tsx?$": [
"ts-jest",
{}
]
}
},
"dependencies": {
@@ -62,8 +69,9 @@
"form-data@<4.0.6": ">=4.0.6",
"uuid@<11.1.1": ">=11.1.1",
"esbuild": ">=0.28.1",
"undici@<6.27.0": ">=6.27.0 <8.0.0",
"undici@>=8.0.0 <8.5.0": ">=8.5.0"
"undici@<7.29.0": ">=7.29.0 <8.0.0",
"undici@>=8.0.0 <8.9.0": ">=8.9.0 <9.0.0",
"brace-expansion@<1.1.16": ">=1.1.16 <2.0.0"
}
}
}
+8 -6
View File
@@ -8,8 +8,9 @@ overrides:
form-data@<4.0.6: '>=4.0.6'
uuid@<11.1.1: '>=11.1.1'
esbuild: '>=0.28.1'
undici@<6.27.0: '>=6.27.0 <8.0.0'
undici@>=8.0.0 <8.5.0: '>=8.5.0'
undici@<7.29.0: '>=7.29.0 <8.0.0'
undici@>=8.0.0 <8.9.0: '>=8.9.0 <9.0.0'
brace-expansion@<1.1.16: '>=1.1.16 <2.0.0'
importers:
@@ -413,8 +414,8 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
brace-expansion@1.1.15:
resolution: {integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==}
brace-expansion@1.1.18:
resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
@@ -530,6 +531,7 @@ packages:
crypto-js@4.2.0:
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
deprecated: Active development of CryptoJS has been discontinued. This library is no longer maintained.
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
@@ -1995,7 +1997,7 @@ snapshots:
baseline-browser-mapping@2.10.41: {}
brace-expansion@1.1.15:
brace-expansion@1.1.18:
dependencies:
balanced-match: 1.0.2
concat-map: 0.0.1
@@ -2792,7 +2794,7 @@ snapshots:
minimatch@3.1.5:
dependencies:
brace-expansion: 1.1.15
brace-expansion: 1.1.18
minimist@1.2.8: {}
+3 -2
View File
@@ -5,5 +5,6 @@ overrides:
"form-data@<4.0.6": ">=4.0.6"
"uuid@<11.1.1": ">=11.1.1"
"esbuild": ">=0.28.1"
"undici@<6.27.0": ">=6.27.0 <8.0.0"
"undici@>=8.0.0 <8.5.0": ">=8.5.0"
"undici@<7.29.0": ">=7.29.0 <8.0.0"
"undici@>=8.0.0 <8.9.0": ">=8.9.0 <9.0.0"
"brace-expansion@<1.1.16": ">=1.1.16 <2.0.0"
+4 -3
View File
@@ -292,18 +292,19 @@
"glob@>=10.2.0 <10.5.0": "^10.5.0",
"@modelcontextprotocol/sdk": "^1.25.4",
"esbuild": ">=0.28.1",
"undici@<6.27.0": ">=6.27.0 <8.0.0",
"undici@<7.29.0": ">=7.29.0 <8.0.0",
"@aws-sdk/client-bedrock-runtime": "3.967.0",
"@aws-sdk/client-neptune-graph": "3.966.0",
"axios@<1.18.0": ">=1.18.0 <2.0.0",
"brace-expansion@<1.1.16": ">=1.1.16 <2.0.0",
"brace-expansion@>=2.0.0 <2.1.2": ">=2.1.2 <3.0.0",
"brace-expansion@>=2.0.0 <2.1.4": ">=2.1.4 <3.0.0",
"brace-expansion@>=3.0.0 <5.0.8": ">=5.0.8 <6.0.0",
"fast-xml-parser@>=5.0.0 <5.10.1": ">=5.10.1 <6.0.0",
"tar@>=7.0.0 <7.5.21": ">=7.5.21 <8.0.0",
"thrift@<0.23.0": "^0.23.0",
"mongoose@>=9.0.0 <9.7.2": ">=9.7.2 <10.0.0",
"protobufjs@<7.6.5": ">=7.6.5 <8.0.0"
"protobufjs@<7.6.5": ">=7.6.5 <8.0.0",
"ip-address@<10.3.1": ">=10.3.1 <11.0.0"
}
}
}
+17 -16
View File
@@ -25,18 +25,19 @@ overrides:
glob@>=10.2.0 <10.5.0: ^10.5.0
'@modelcontextprotocol/sdk': ^1.25.4
esbuild: '>=0.28.1'
undici@<6.27.0: '>=6.27.0 <8.0.0'
undici@<7.29.0: '>=7.29.0 <8.0.0'
'@aws-sdk/client-bedrock-runtime': 3.967.0
'@aws-sdk/client-neptune-graph': 3.966.0
axios@<1.18.0: '>=1.18.0 <2.0.0'
brace-expansion@<1.1.16: '>=1.1.16 <2.0.0'
brace-expansion@>=2.0.0 <2.1.2: '>=2.1.2 <3.0.0'
brace-expansion@>=2.0.0 <2.1.4: '>=2.1.4 <3.0.0'
brace-expansion@>=3.0.0 <5.0.8: '>=5.0.8 <6.0.0'
fast-xml-parser@>=5.0.0 <5.10.1: '>=5.10.1 <6.0.0'
tar@>=7.0.0 <7.5.21: '>=7.5.21 <8.0.0'
thrift@<0.23.0: ^0.23.0
mongoose@>=9.0.0 <9.7.2: '>=9.7.2 <10.0.0'
protobufjs@<7.6.5: '>=7.6.5 <8.0.0'
ip-address@<10.3.1: '>=10.3.1 <11.0.0'
importers:
@@ -2165,8 +2166,8 @@ packages:
brace-expansion@1.1.16:
resolution: {integrity: sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==}
brace-expansion@2.1.2:
resolution: {integrity: sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==}
brace-expansion@2.1.4:
resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==}
brace-expansion@5.0.8:
resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==}
@@ -3027,8 +3028,8 @@ packages:
resolution: {integrity: sha512-4rTJX6Q5wTYEvxboXi8DsEiUo+OvqJGtLYOSGm37KpdRXsG5XJjbVtYKGJpPSWP+QT7rWscA4vsrdmzbEbenpw==}
engines: {node: '>=18.12.0'}
ip-address@10.2.0:
resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==}
ip-address@10.4.0:
resolution: {integrity: sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==}
engines: {node: '>= 12'}
is-arrayish@0.2.1:
@@ -4626,8 +4627,8 @@ packages:
undici-types@7.24.6:
resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
undici@7.28.0:
resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==}
undici@7.29.0:
resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==}
engines: {node: '>=20.18.1'}
update-browserslist-db@1.2.3:
@@ -6155,7 +6156,7 @@ snapshots:
ms: 2.1.3
secure-json-parse: 4.1.0
tslib: 2.8.1
undici: 7.28.0
undici: 7.29.0
transitivePeerDependencies:
- supports-color
@@ -6711,7 +6712,7 @@ snapshots:
dependencies:
'@qdrant/openapi-typescript-fetch': 1.2.6
typescript: 5.5.4
undici: 7.28.0
undici: 7.29.0
'@qdrant/openapi-typescript-fetch@1.2.6': {}
@@ -7183,7 +7184,7 @@ snapshots:
'@turbopuffer/turbopuffer@2.5.0':
dependencies:
pako: 2.2.0
undici: 7.28.0
undici: 7.29.0
'@types/babel__core@7.20.5':
dependencies:
@@ -7542,7 +7543,7 @@ snapshots:
balanced-match: 1.0.2
concat-map: 0.0.1
brace-expansion@2.1.2:
brace-expansion@2.1.4:
dependencies:
balanced-match: 1.0.2
@@ -8448,7 +8449,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
ip-address@10.2.0: {}
ip-address@10.4.0: {}
is-arrayish@0.2.1: {}
@@ -9076,7 +9077,7 @@ snapshots:
minimatch@9.0.9:
dependencies:
brace-expansion: 2.1.2
brace-expansion: 2.1.4
minimist@1.2.8: {}
@@ -9886,7 +9887,7 @@ snapshots:
socks@2.8.9:
dependencies:
ip-address: 10.2.0
ip-address: 10.4.0
smart-buffer: 4.2.0
source-map-support@0.5.13:
@@ -10232,7 +10233,7 @@ snapshots:
undici-types@7.24.6: {}
undici@7.28.0: {}
undici@7.29.0: {}
update-browserslist-db@1.2.3(browserslist@4.28.2):
dependencies:
+3 -2
View File
@@ -25,13 +25,14 @@ overrides:
"glob@>=10.2.0 <10.5.0": "^10.5.0"
"@modelcontextprotocol/sdk": "^1.25.4"
"esbuild": ">=0.28.1"
"undici@<6.27.0": ">=6.27.0 <8.0.0"
"undici@<7.29.0": ">=7.29.0 <8.0.0"
"axios@<1.18.0": ">=1.18.0 <2.0.0"
"brace-expansion@<1.1.16": ">=1.1.16 <2.0.0"
"brace-expansion@>=2.0.0 <2.1.2": ">=2.1.2 <3.0.0"
"brace-expansion@>=2.0.0 <2.1.4": ">=2.1.4 <3.0.0"
"brace-expansion@>=3.0.0 <5.0.8": ">=5.0.8 <6.0.0"
"fast-xml-parser@>=5.0.0 <5.10.1": ">=5.10.1 <6.0.0"
"tar@>=7.0.0 <7.5.21": ">=7.5.21 <8.0.0"
"thrift@<0.23.0": "^0.23.0"
"mongoose@>=9.0.0 <9.7.2": ">=9.7.2 <10.0.0"
"protobufjs@<7.6.5": ">=7.6.5 <8.0.0"
"ip-address@<10.3.1": ">=10.3.1 <11.0.0"
+42 -19
View File
@@ -1,4 +1,4 @@
import type { Connection, Pool } from "oracledb";
import type { BindParameters, Connection, Pool } from "oracledb";
import { v4 as uuidv4 } from "uuid";
import { VectorStore } from "./base";
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
@@ -401,7 +401,15 @@ export class OracleAIVectorSearch implements VectorStore {
async initialize(): Promise<void> {
if (!this._initPromise) {
this._initPromise = this._doInitialize();
this._initPromise = this._doInitialize().catch(async (error) => {
if (this.ownsClient && this.client) {
await Promise.resolve(this.client.close()).catch(() => {});
this.client = undefined;
this.ownsClient = false;
}
this._initPromise = undefined;
throw error;
});
}
return this._initPromise;
}
@@ -565,19 +573,33 @@ export class OracleAIVectorSearch implements VectorStore {
ids: string[],
payloads: Record<string, any>[],
): Promise<void> {
if (ids.length !== vectors.length) {
throw new Error("ids and vectors must have the same length");
}
if (payloads.length !== vectors.length) {
throw new Error("payloads and vectors must have the same length");
}
if (vectors.length === 0) return;
await this.initialize();
await this.withConnection(async (connection) => {
for (let i = 0; i < vectors.length; i++) {
await connection.execute(
`INSERT INTO ${this.collectionName} (id, vector, payload) VALUES (:id, :vector, :payload)`,
{
id: ids[i],
vector: this.vectorBind(vectors[i]),
payload: this.payloadBind(payloads[i] ?? {}),
await connection.executeMany(
`INSERT INTO ${this.collectionName} (id, vector, payload) VALUES (:id, :vector, :payload)`,
vectors.map((vector, i) => ({
id: ids[i],
vector: new Float32Array(vector),
payload: payloads[i] ?? {},
})) as BindParameters[],
{
bindDefs: {
id: { type: this.oracledb.DB_TYPE_VARCHAR, maxSize: 36 },
vector: { type: this.oracledb.DB_TYPE_VECTOR },
payload: { type: this.oracledb.DB_TYPE_JSON },
},
);
}
},
);
}, true);
}
@@ -589,8 +611,12 @@ export class OracleAIVectorSearch implements VectorStore {
await this.initialize();
const [whereClause, filterBinds] = buildWhereClause(filters);
const hasFilter = whereClause.length > 0;
const selectClause = hasFilter
? "SELECT"
: `SELECT /*+ VECTOR_INDEX_TRANSFORM(${this.collectionName}) */`;
const sql =
`SELECT id, payload, VECTOR_DISTANCE(vector, :query_vec, ${this.distanceMetric}) distance ` +
`${selectClause} id, payload, VECTOR_DISTANCE(vector, :query_vec, ${this.distanceMetric}) distance ` +
`FROM ${this.collectionName} ${whereClause} ORDER BY distance FETCH APPROX FIRST :max_rows ROWS ONLY`;
const rows = await this.withConnection(async (connection) => {
@@ -687,20 +713,17 @@ export class OracleAIVectorSearch implements VectorStore {
return this.withConnection(async (connection) => {
const listResult = await connection.execute<any[]>(
`SELECT id, payload FROM ${this.collectionName} ${whereClause} FETCH FIRST :max_rows ROWS ONLY`,
`SELECT id, payload, COUNT(*) OVER () total FROM ${this.collectionName} ${whereClause} FETCH FIRST :max_rows ROWS ONLY`,
{ ...filterBinds, max_rows: topK },
);
const countResult = await connection.execute<any[]>(
`SELECT COUNT(*) FROM ${this.collectionName} ${whereClause}`,
filterBinds,
);
const results = (listResult.rows ?? []).map((row) => ({
const rows = listResult.rows ?? [];
const results = rows.map((row) => ({
id: row[0],
payload: this.loadPayload(row[1]),
}));
return [results, Number(countResult.rows?.[0]?.[0] ?? 0)];
return [results, Number(rows[0]?.[2] ?? 0)];
});
}
+162 -15
View File
@@ -2,10 +2,21 @@
/** Oracle AI Vector Search filter, config and SQL tests. The driver is mocked, so no database is needed. */
const DB_TYPE_VECTOR = { name: "DB_TYPE_VECTOR" };
const DB_TYPE_JSON = { name: "DB_TYPE_JSON" };
const DB_TYPE_VARCHAR = { name: "DB_TYPE_VARCHAR" };
jest.mock("oracledb", () => ({ thin: true, DB_TYPE_VECTOR, DB_TYPE_JSON }), {
virtual: true,
});
const mockCreatePool = jest.fn();
jest.mock(
"oracledb",
() => ({
thin: true,
DB_TYPE_VECTOR,
DB_TYPE_JSON,
DB_TYPE_VARCHAR,
createPool: mockCreatePool,
}),
{ virtual: true },
);
import {
OracleAIVectorSearch,
@@ -13,7 +24,7 @@ import {
quoteIdentifier,
} from "../src/vector_stores/oracledb";
type Call = { sql: string; binds: any };
type Call = { sql: string; binds: any; options?: any };
function fakeConnection(calls: Call[], resultsBySql: Array<any[][]>) {
let selectIndex = 0;
@@ -26,6 +37,10 @@ function fakeConnection(calls: Call[], resultsBySql: Array<any[][]>) {
}
return { rows: [] };
},
async executeMany(sql: string, binds: any[], options: any = {}) {
calls.push({ sql: sql.replace(/\s+/g, " ").trim(), binds, options });
return { rows: [] };
},
async commit() {},
async rollback() {},
async close() {},
@@ -249,13 +264,56 @@ describe("OracleAIVectorSearch SQL", () => {
await makeStore(calls).insert([[1, 2, 3]], ["id-1"], [{ data: "hello" }]);
const insert = calls.find((c) => c.sql.startsWith("INSERT INTO"))!;
expect(insert.binds.id).toBe("id-1");
expect(insert.binds.vector.type).toBe(DB_TYPE_VECTOR);
expect(insert.binds.vector.val).toEqual(new Float32Array([1, 2, 3]));
expect(insert.binds.payload).toEqual({
type: DB_TYPE_JSON,
val: { data: "hello" },
expect(insert.binds).toEqual([
{
id: "id-1",
vector: new Float32Array([1, 2, 3]),
payload: { data: "hello" },
},
]);
expect(insert.options.bindDefs.id).toEqual({
type: DB_TYPE_VARCHAR,
maxSize: 36,
});
expect(insert.options.bindDefs.vector.type).toBe(DB_TYPE_VECTOR);
expect(insert.options.bindDefs.payload.type).toBe(DB_TYPE_JSON);
});
it("issues a single executeMany call with one bind row per vector on a multi-row insert", async () => {
const calls: Call[] = [];
await makeStore(calls).insert(
[
[1, 2, 3],
[4, 5, 6],
],
["id-1", "id-2"],
[{ a: 1 }, { b: 2 }],
);
const inserts = calls.filter((c) => c.sql.startsWith("INSERT INTO"));
expect(inserts).toHaveLength(1);
expect(inserts[0].binds).toHaveLength(2);
expect(inserts[0].binds[0].id).toBe("id-1");
expect(inserts[0].binds[1].id).toBe("id-2");
});
it("issues no insert statement when inserting an empty batch", async () => {
const calls: Call[] = [];
await makeStore(calls).insert([], [], []);
expect(calls.filter((c) => c.sql.startsWith("INSERT INTO"))).toHaveLength(
0,
);
});
it("rejects insert batches whose IDs or payloads do not match vectors", async () => {
const store = makeStore([]);
await expect(store.insert([[1, 2, 3]], [], [{}])).rejects.toThrow(
"ids and vectors must have the same length",
);
await expect(store.insert([[1, 2, 3]], ["id-1"], [])).rejects.toThrow(
"payloads and vectors must have the same length",
);
});
it("converts cosine distance to a similarity score", async () => {
@@ -266,7 +324,7 @@ describe("OracleAIVectorSearch SQL", () => {
expect(results).toEqual([
{ id: "id-1", payload: { data: "hello" }, score: 0.75 },
]);
const select = calls.find((c) => c.sql.startsWith("SELECT id, payload,"))!;
const select = calls.find((c) => c.sql.includes("VECTOR_DISTANCE"))!;
expect(select.sql).toContain(
"VECTOR_DISTANCE(vector, :query_vec, COSINE) distance",
);
@@ -282,6 +340,24 @@ describe("OracleAIVectorSearch SQL", () => {
expect(result.score).toBeCloseTo(0.4);
});
it("adds the VECTOR_INDEX_TRANSFORM hint when searching without filters", async () => {
const calls: Call[] = [];
const store = makeStore(calls, [[["id-1", {}, 0.1]]]);
await store.search([1, 2, 3], 5);
const select = calls.find((c) => c.sql.includes("VECTOR_DISTANCE"))!;
expect(select.sql).toContain('/*+ VECTOR_INDEX_TRANSFORM("mem0") */');
});
it("omits the VECTOR_INDEX_TRANSFORM hint when searching with filters", async () => {
const calls: Call[] = [];
const store = makeStore(calls, [[["id-1", {}, 0.1]]]);
await store.search([1, 2, 3], 5, { user_id: "alice" });
const select = calls.find((c) => c.sql.includes("VECTOR_DISTANCE"))!;
expect(select.sql).not.toContain("VECTOR_INDEX_TRANSFORM");
});
it("parses a payload returned as a JSON string", async () => {
const store = makeStore([], [[["id-1", '{"data":"hello"}']]]);
expect(await store.get("id-1")).toEqual({
@@ -313,16 +389,87 @@ describe("OracleAIVectorSearch SQL", () => {
it("returns rows and the total count from list", async () => {
const calls: Call[] = [];
const store = makeStore(calls, [[["id-1", { data: "hello" }]], [[7]]]);
const store = makeStore(calls, [[["id-1", { data: "hello" }, 7]]]);
const [results, count] = await store.list({ user_id: "alice" }, 10);
expect(results).toEqual([{ id: "id-1", payload: { data: "hello" } }]);
expect(count).toBe(7);
const list = calls.find((c) =>
c.sql.startsWith("SELECT id, payload FROM"),
)!;
const selects = calls.filter((c) => /^SELECT/i.test(c.sql));
expect(selects).toHaveLength(1);
const [list] = selects;
expect(list.sql).toContain(
"SELECT id, payload, COUNT(*) OVER () total FROM",
);
expect(list.sql).toContain("WHERE JSON_EXISTS(payload,");
expect(list.binds).toEqual({ f_0: "alice", max_rows: 10 });
});
it("returns a total of 0 from list when no rows match", async () => {
const store = makeStore([], [[]]);
const [results, count] = await store.list();
expect(results).toEqual([]);
expect(count).toBe(0);
});
});
describe("OracleAIVectorSearch initialization failure", () => {
beforeEach(() => mockCreatePool.mockReset());
function fakePool(serverVersion: string) {
const connection = {
...fakeConnection([], []),
oracleServerVersionString: serverVersion,
};
return {
close: jest.fn(async () => {}),
getConnection: jest.fn(async () => connection),
};
}
function makePoolStore() {
return new OracleAIVectorSearch({
connectionParams: { user: "u", password: "p", connectString: "d" },
collectionName: "mem0",
embeddingModelDims: 3,
} as any);
}
it("closes the pool it owns when initialization fails", async () => {
const pool = fakePool("23.3.0.24.05");
mockCreatePool.mockResolvedValue(pool);
await expect(makePoolStore().initialize()).rejects.toThrow(
"Oracle DB version 23.3.0.24.05 not supported",
);
expect(pool.close).toHaveBeenCalledTimes(1);
});
it("does not cache the rejection, so a later attempt can succeed", async () => {
const failing = fakePool("23.3.0.24.05");
const working = fakePool("23.4.0.24.05");
mockCreatePool
.mockResolvedValueOnce(failing)
.mockResolvedValueOnce(working);
const store = makePoolStore();
await expect(store.initialize()).rejects.toThrow("not supported");
await expect(store.initialize()).resolves.toBeUndefined();
expect(mockCreatePool).toHaveBeenCalledTimes(2);
});
it("leaves a caller-supplied client open when initialization fails", async () => {
const client = fakeConnection([], []);
client.oracleServerVersionString = "23.3.0.24.05";
const close = jest.spyOn(client, "close");
const store = new OracleAIVectorSearch({
client: client as any,
collectionName: "mem0",
embeddingModelDims: 3,
} as any);
await expect(store.initialize()).rejects.toThrow("not supported");
expect(close).not.toHaveBeenCalled();
});
});
+11
View File
@@ -791,6 +791,11 @@ class Memory(MemoryBase):
are treated as general conversational/factual memories.
prompt (str, optional): Prompt to use for the memory creation. Defaults to None.
Note:
`search()` and `get_all()` scope queries via `filters={"user_id": "...", "agent_id": "...", "run_id": "..."}` —
they reject top-level `user_id`/`agent_id`/`run_id` arguments. `add()` accepts them top-level, but passing
the same arguments to `search()`/`get_all()` raises a `ValueError`; use the `filters` form there instead.
Returns:
dict: A dictionary containing the result of the memory addition operation, typically
@@ -2447,6 +2452,12 @@ class AsyncMemory(MemoryBase):
Pass "procedural_memory" to create procedural memories.
prompt (str, optional): Prompt to use for the memory creation. Defaults to None.
llm (BaseChatModel, optional): LLM class to use for generating procedural memories. Defaults to None. Useful when user is using LangChain ChatModel.
Note:
`search()` and `get_all()` scope queries via `filters={"user_id": "...", "agent_id": "...", "run_id": "..."}` —
they reject top-level `user_id`/`agent_id`/`run_id` arguments. `add()` accepts them top-level, but passing
the same arguments to `search()`/`get_all()` raises a `ValueError`; use the `filters` form there instead.
Returns:
dict: A dictionary containing the result of the memory addition operation.
"""
+26 -17
View File
@@ -245,25 +245,34 @@ class OracleAIVectorSearch(VectorStoreBase):
self.client = oracledb.connect(**self.config.connection_params)
self._owns_client = True
if not (hasattr(self.client, "thin") and self.client.thin):
if oracledb.clientversion()[:2] < (23, 4):
raise RuntimeError(
f"Oracle DB client driver version {'.'.join(map(str, oracledb.clientversion()))} "
"not supported, must be >=23.4 for vector support"
try:
if not (hasattr(self.client, "thin") and self.client.thin):
if oracledb.clientversion()[:2] < (23, 4):
raise RuntimeError(
f"Oracle DB client driver version {'.'.join(map(str, oracledb.clientversion()))} "
"not supported, must be >=23.4 for vector support"
)
if isinstance(self.client, oracledb.Connection):
db_version = tuple([int(v) for v in self.client.version.split(".")])
else:
with self.client.acquire() as conn:
db_version = tuple([int(v) for v in conn.version.split(".")])
if db_version < (23, 4):
raise ValueError(
f"Oracle DB version {'.'.join(map(str, db_version))} not supported, "
"must be >=23.4 for vector support"
)
if isinstance(self.client, oracledb.Connection):
db_version = tuple([int(v) for v in self.client.version.split(".")])
else:
with self.client.acquire() as conn:
db_version = tuple([int(v) for v in conn.version.split(".")])
if db_version < (23, 4):
raise ValueError(
f"Oracle DB version {'.'.join(map(str, db_version))} not supported, must be >=23.4 for vector support"
)
self.create_col()
self.create_col()
except Exception:
if self._owns_client:
try:
self.client.close()
except Exception:
pass
raise
@contextmanager
def _get_cursor(self, commit: bool = False):
+21
View File
@@ -331,6 +331,27 @@ def test_config_rejects_none_for_non_optional_fields(field, value):
OracleAIVectorSearchConfig(client=object(), **{field: value})
def test_init_closes_owned_client_when_post_connect_setup_fails(monkeypatch):
fake_connection = MagicMock(spec=oracledb.Connection)
fake_connection.thin = True
fake_connection.version = "23.4.0.0"
monkeypatch.setattr(oracledb, "connect", MagicMock(return_value=fake_connection))
monkeypatch.setattr(OracleAIVectorSearch, "create_col", MagicMock(side_effect=RuntimeError("boom")))
monkeypatch.setattr(OracleAIVectorSearch, "__del__", lambda self: None)
with pytest.raises(RuntimeError, match="boom"):
OracleAIVectorSearch(
collection_name=_unique_collection_name(),
embedding_model_dims=DIM,
connection_params={"user": "u", "password": "p", "dsn": "d"},
use_connection_pool=False,
do_create_index=False,
)
fake_connection.close.assert_called_once()
@pytest.mark.parametrize(
("metric", "distance", "expected_score"),
[