@mastra/upstash 1.4.1 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # @mastra/upstash
2
2
 
3
+ ## 1.4.2
4
+
5
+ ### Patch Changes
6
+
7
+ - Fixed concurrent resume() calls on the same suspended workflow run executing downstream steps more than once. A resume now atomically claims the run before executing anything, so only one caller continues a given suspension. Losing callers throw WORKFLOW_RESUME_ALREADY_CLAIMED without running any steps. Fixes #20443 ([#21725](https://github.com/mastra-ai/mastra/pull/21725))
8
+
9
+ - Workflow state updates now support an optional expectedStatus guard, so a status change is only applied when the stored run is in an expected state. This is what makes concurrent workflow resumes safe. ([#21725](https://github.com/mastra-ai/mastra/pull/21725))
10
+
11
+ - Resume conflicts now return 409 Conflict. When a suspended workflow run has already been resumed by another caller, the resume endpoints respond with 409 instead of a generic error. ([#21725](https://github.com/mastra-ai/mastra/pull/21725))
12
+
13
+ - Updated dependencies [[`88d14ca`](https://github.com/mastra-ai/mastra/commit/88d14cac008582a618fecc3d5c7fd3bdf4f6ddc3), [`480e491`](https://github.com/mastra-ai/mastra/commit/480e491588bd6a7a1c9ee4407590ad625dd33952), [`9267e9b`](https://github.com/mastra-ai/mastra/commit/9267e9b3d9c2fcf16936050495a787054c2431ab), [`acc3471`](https://github.com/mastra-ai/mastra/commit/acc3471de5f3fde8027ee4e355af292b2bc1bc30), [`b6a771e`](https://github.com/mastra-ai/mastra/commit/b6a771ef23d203ddb348efca8065eff65def8191), [`84a5b69`](https://github.com/mastra-ai/mastra/commit/84a5b699f84d6bae0a34efe5a970d891090b9f41), [`9267e9b`](https://github.com/mastra-ai/mastra/commit/9267e9b3d9c2fcf16936050495a787054c2431ab), [`3bb88dd`](https://github.com/mastra-ai/mastra/commit/3bb88ddf07fb98f3cd16d3bff94e51cd3b45d011), [`d23e75d`](https://github.com/mastra-ai/mastra/commit/d23e75d57cc7cf5b9bfdbee896bf5a6a2484fed7), [`c8faa4e`](https://github.com/mastra-ai/mastra/commit/c8faa4e1cfebaec56b65e754e90b9fe46d153359), [`d378d75`](https://github.com/mastra-ai/mastra/commit/d378d7511f71309ed61a8f6b93cd0361dc6cb70f), [`84a5b69`](https://github.com/mastra-ai/mastra/commit/84a5b699f84d6bae0a34efe5a970d891090b9f41), [`26d4016`](https://github.com/mastra-ai/mastra/commit/26d40160ff7f7d8bf95fee2039a52cbc83863533), [`7c60df5`](https://github.com/mastra-ai/mastra/commit/7c60df5c7872343fbac5c3e5b1175c8076a5abfd), [`9267e9b`](https://github.com/mastra-ai/mastra/commit/9267e9b3d9c2fcf16936050495a787054c2431ab), [`84a5b69`](https://github.com/mastra-ai/mastra/commit/84a5b699f84d6bae0a34efe5a970d891090b9f41), [`f2031a4`](https://github.com/mastra-ai/mastra/commit/f2031a47445e8f67a89ba1309036816f97ab7a65), [`9267e9b`](https://github.com/mastra-ai/mastra/commit/9267e9b3d9c2fcf16936050495a787054c2431ab), [`cad4208`](https://github.com/mastra-ai/mastra/commit/cad42082e6aa1776168a94914f523334be45d929), [`8e529d4`](https://github.com/mastra-ai/mastra/commit/8e529d4ac754efef04b225841349e0da9edf89a6), [`57c5103`](https://github.com/mastra-ai/mastra/commit/57c51035a2a36e3df3c4f32f46bb789a66ed5946), [`038b7b4`](https://github.com/mastra-ai/mastra/commit/038b7b405cb4ac25ab3f3031334111b1f87ac112), [`4132d61`](https://github.com/mastra-ai/mastra/commit/4132d61f8367077120ee9e6420d3224dffd93c93), [`d378d75`](https://github.com/mastra-ai/mastra/commit/d378d7511f71309ed61a8f6b93cd0361dc6cb70f)]:
14
+ - @mastra/core@1.61.0
15
+ - @mastra/redis@1.4.2
16
+
17
+ ## 1.4.2-alpha.0
18
+
19
+ ### Patch Changes
20
+
21
+ - Fixed concurrent resume() calls on the same suspended workflow run executing downstream steps more than once. A resume now atomically claims the run before executing anything, so only one caller continues a given suspension. Losing callers throw WORKFLOW_RESUME_ALREADY_CLAIMED without running any steps. Fixes #20443 ([#21725](https://github.com/mastra-ai/mastra/pull/21725))
22
+
23
+ - Workflow state updates now support an optional expectedStatus guard, so a status change is only applied when the stored run is in an expected state. This is what makes concurrent workflow resumes safe. ([#21725](https://github.com/mastra-ai/mastra/pull/21725))
24
+
25
+ - Resume conflicts now return 409 Conflict. When a suspended workflow run has already been resumed by another caller, the resume endpoints respond with 409 instead of a generic error. ([#21725](https://github.com/mastra-ai/mastra/pull/21725))
26
+
27
+ - Updated dependencies [[`88d14ca`](https://github.com/mastra-ai/mastra/commit/88d14cac008582a618fecc3d5c7fd3bdf4f6ddc3), [`84a5b69`](https://github.com/mastra-ai/mastra/commit/84a5b699f84d6bae0a34efe5a970d891090b9f41), [`84a5b69`](https://github.com/mastra-ai/mastra/commit/84a5b699f84d6bae0a34efe5a970d891090b9f41), [`84a5b69`](https://github.com/mastra-ai/mastra/commit/84a5b699f84d6bae0a34efe5a970d891090b9f41), [`038b7b4`](https://github.com/mastra-ai/mastra/commit/038b7b405cb4ac25ab3f3031334111b1f87ac112), [`4132d61`](https://github.com/mastra-ai/mastra/commit/4132d61f8367077120ee9e6420d3224dffd93c93)]:
28
+ - @mastra/core@1.60.1-alpha.0
29
+ - @mastra/redis@1.4.2-alpha.0
30
+
3
31
  ## 1.4.1
4
32
 
5
33
  ### Patch Changes
@@ -3,7 +3,7 @@ name: mastra-upstash
3
3
  description: Documentation for @mastra/upstash. Use when working with @mastra/upstash APIs, configuration, or implementation.
4
4
  metadata:
5
5
  package: "@mastra/upstash"
6
- version: "1.4.1"
6
+ version: "1.4.2"
7
7
  ---
8
8
 
9
9
  ## When to use
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.4.1",
2
+ "version": "1.4.2",
3
3
  "package": "@mastra/upstash",
4
4
  "exports": {},
5
5
  "modules": {}
@@ -77,9 +77,19 @@ For detailed information about available operators and syntax, see the [Metadata
77
77
 
78
78
  Basic filtering examples:
79
79
 
80
+ **MongoDB**:
81
+
80
82
  ```ts
83
+ import { MongoDBVector } from '@mastra/mongodb'
84
+
85
+ const mongoVector = new MongoDBVector({
86
+ id: 'mongodb-vector',
87
+ uri: process.env.MONGODB_URI,
88
+ dbName: process.env.MONGODB_DB_NAME,
89
+ })
90
+
81
91
  // Simple equality filter
82
- const results = await pgVector.query({
92
+ const equalityResults = await mongoVector.query({
83
93
  indexName: 'embeddings',
84
94
  queryVector: embedding,
85
95
  topK: 10,
@@ -89,7 +99,7 @@ const results = await pgVector.query({
89
99
  })
90
100
 
91
101
  // Numeric comparison
92
- const results = await pgVector.query({
102
+ const priceResults = await mongoVector.query({
93
103
  indexName: 'embeddings',
94
104
  queryVector: embedding,
95
105
  topK: 10,
@@ -99,7 +109,7 @@ const results = await pgVector.query({
99
109
  })
100
110
 
101
111
  // Multiple conditions
102
- const results = await pgVector.query({
112
+ const compoundResults = await mongoVector.query({
103
113
  indexName: 'embeddings',
104
114
  queryVector: embedding,
105
115
  topK: 10,
@@ -111,7 +121,7 @@ const results = await pgVector.query({
111
121
  })
112
122
 
113
123
  // Array operations
114
- const results = await pgVector.query({
124
+ const tagResults = await mongoVector.query({
115
125
  indexName: 'embeddings',
116
126
  queryVector: embedding,
117
127
  topK: 10,
@@ -121,7 +131,64 @@ const results = await pgVector.query({
121
131
  })
122
132
 
123
133
  // Logical operators
124
- const results = await pgVector.query({
134
+ const categoryResults = await mongoVector.query({
135
+ indexName: 'embeddings',
136
+ queryVector: embedding,
137
+ topK: 10,
138
+ filter: {
139
+ $or: [{ category: 'electronics' }, { category: 'accessories' }],
140
+ $and: [{ price: { $gt: 50 } }, { price: { $lt: 200 } }],
141
+ },
142
+ })
143
+ ```
144
+
145
+ **pgVector**:
146
+
147
+ ```ts
148
+ // Simple equality filter
149
+ const equalityResults = await pgVector.query({
150
+ indexName: 'embeddings',
151
+ queryVector: embedding,
152
+ topK: 10,
153
+ filter: {
154
+ source: 'article1.txt',
155
+ },
156
+ })
157
+
158
+ // Numeric comparison
159
+ const priceResults = await pgVector.query({
160
+ indexName: 'embeddings',
161
+ queryVector: embedding,
162
+ topK: 10,
163
+ filter: {
164
+ price: { $gt: 100 },
165
+ },
166
+ })
167
+
168
+ // Multiple conditions
169
+ const compoundResults = await pgVector.query({
170
+ indexName: 'embeddings',
171
+ queryVector: embedding,
172
+ topK: 10,
173
+ filter: {
174
+ category: 'electronics',
175
+ price: { $lt: 1000 },
176
+ inStock: true,
177
+ },
178
+ })
179
+
180
+ // Array operations
181
+ const tagResults = await pgVector.query({
182
+ indexName: 'embeddings',
183
+ queryVector: embedding,
184
+ topK: 10,
185
+ filter: {
186
+ tags: { $in: ['sale', 'new'] },
187
+ },
188
+ })
189
+
190
+ // Logical operators
191
+ const categoryResults = await pgVector.query({
125
192
  indexName: 'embeddings',
126
193
  queryVector: embedding,
127
194
  topK: 10,
@@ -141,6 +208,47 @@ Common use cases for metadata filtering:
141
208
  - Combine multiple conditions for precise querying
142
209
  - Filter by document attributes (e.g., language, author)
143
210
 
211
+ ### Where the filter is applied
212
+
213
+ Vector stores differ in _when_ they apply a metadata filter, which affects how filtered queries scale.
214
+
215
+ MongoDB can evaluate the filter inside the vector index itself. This keeps the query on a single round trip to `$vectorSearch`, so it avoids the pre-filter pass that collects matching document IDs and the 16 MB BSON limit that pass is subject to. Declaring the fields you filter on in `filterFields` when you create the index is what enables it:
216
+
217
+ ```ts
218
+ // Declare the metadata fields you want to filter on
219
+ await mongoVector.createIndex({
220
+ indexName: 'embeddings',
221
+ dimension: 1536,
222
+ filterFields: ['source', 'price', 'category', 'inStock', 'tags'],
223
+ })
224
+
225
+ // createIndex() returns before the index finishes building
226
+ await mongoVector.waitForIndexReady({ indexName: 'embeddings' })
227
+
228
+ // The filter is applied during the index search
229
+ const results = await mongoVector.query({
230
+ indexName: 'embeddings',
231
+ queryVector: embedding,
232
+ topK: 10,
233
+ filter: { source: 'article1.txt' },
234
+ })
235
+ ```
236
+
237
+ Mastra passes the filter to the index only when every field it references is declared in `filterFields` and every operator is one the index accepts: `$and`, `$or`, `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, and `$nin`. A filter that uses an undeclared field or any other operator takes a fallback path: Mastra matches the collection first and passes the matching document IDs into the vector search. That fallback holds only while the ID set fits within MongoDB's 16 MB BSON document limit. On large collections the query fails once that limit is exceeded, so declare your filter fields when you expect selective filters over large data sets.
238
+
239
+ pgVector applies the filter as an ordinary query condition:
240
+
241
+ ```ts
242
+ const results = await pgVector.query({
243
+ indexName: 'embeddings',
244
+ queryVector: embedding,
245
+ topK: 10,
246
+ filter: { source: 'article1.txt' },
247
+ })
248
+ ```
249
+
250
+ Postgres vector indexes (HNSW and IVFFlat) can't restrict that search to rows matching a condition. When a filter is present, pgVector instead compares the query vector against every matching row and returns the closest `topK`. Results are exact, but the work grows with the number of rows the filter matches. Indexing the metadata column speeds up row retrieval. The distance comparisons still happen per row.
251
+
144
252
  ### Vector Query Tool
145
253
 
146
254
  Sometimes you want to give your agent the ability to query a vector database directly. The Vector Query Tool allows your agent to be in charge of retrieval decisions, combining semantic search with optional filtering and reranking based on the agent's understanding of the user's needs.
package/dist/index.cjs CHANGED
@@ -1729,10 +1729,11 @@ var WorkflowsUpstash = class extends _mastra_core_storage.WorkflowsStorage {
1729
1729
  run_id: runId
1730
1730
  });
1731
1731
  const now = (/* @__PURE__ */ new Date()).toISOString();
1732
- const resultJson = await this.client.eval(`
1732
+ const luaScript = `
1733
1733
  local key = KEYS[1]
1734
1734
  local optsJson = ARGV[1]
1735
1735
  local now = ARGV[2]
1736
+ local expectedStatusJson = ARGV[3]
1736
1737
 
1737
1738
  -- Get existing data
1738
1739
  local existing = redis.call('GET', key)
@@ -1752,6 +1753,21 @@ var WorkflowsUpstash = class extends _mastra_core_storage.WorkflowsStorage {
1752
1753
  return nil
1753
1754
  end
1754
1755
 
1756
+ -- Compare-and-set guard: bail out unless the persisted status is one the caller expects.
1757
+ -- This runs inside the same script as the write, so the check and the write are atomic.
1758
+ if expectedStatusJson ~= '' then
1759
+ local expected = cjson.decode(expectedStatusJson)
1760
+ local matched = false
1761
+ for _, status in ipairs(expected) do
1762
+ if snapshot.status == status then
1763
+ matched = true
1764
+ end
1765
+ end
1766
+ if not matched then
1767
+ return nil
1768
+ end
1769
+ end
1770
+
1755
1771
  -- Merge the new options with the existing snapshot
1756
1772
  local opts = cjson.decode(optsJson)
1757
1773
  for k, v in pairs(opts) do
@@ -1767,7 +1783,14 @@ var WorkflowsUpstash = class extends _mastra_core_storage.WorkflowsStorage {
1767
1783
 
1768
1784
  -- Return the full updated data
1769
1785
  return cjson.encode(data)
1770
- `, [key], [JSON.stringify(opts), now]);
1786
+ `;
1787
+ const { expectedStatus, ...state } = opts;
1788
+ const expectedStatusJson = expectedStatus === void 0 ? "" : JSON.stringify(Array.isArray(expectedStatus) ? expectedStatus : [expectedStatus]);
1789
+ const resultJson = await this.client.eval(luaScript, [key], [
1790
+ JSON.stringify(state),
1791
+ now,
1792
+ expectedStatusJson
1793
+ ]);
1771
1794
  if (!resultJson) return;
1772
1795
  let data;
1773
1796
  if (typeof resultJson === "string") data = JSON.parse(resultJson);