parse-stack-next 5.5.5 → 5.6.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ef7d7de047cc0a3b4a21f3b080745f36f67cd2a53a81787fe53fdbb68fc3ec41
4
- data.tar.gz: 74af45b9e2f334ca13c99419fe71d1653b9a218dffcf2a02ff801088ab86850b
3
+ metadata.gz: d7ea05ce0688ca7c1df6a1077162478dfee52e33d9fac77c7480c5ca818c9eb8
4
+ data.tar.gz: 71e61f7e70da94793dae9d741a3e5d29ffc7f9cd45f2adee6c96cd98b5ac2d24
5
5
  SHA512:
6
- metadata.gz: e31f81f269bf874438617b00a01f1fb095fc7853f544d0a38f02aff8f58e21b14e8561650e3d3fb29bad6ff6276722992451a4f78b640481de3e5ab421cd033a
7
- data.tar.gz: 71063ac383cb393af56969a1b631885585f5863b764218d7bee65b79a34cc9b469a885e96cf5bac5239ec66c140602e2bbe8197a46fa90ff4568cb240571e80f
6
+ metadata.gz: c723fbb9589ce57c7651fab142bed386f3a2070d39fc20887fc40b50f676ccb0041e9b758611e8b75d0a9202095de318ce2bbae35e41015be519209d9d2628dd
7
+ data.tar.gz: dbbc09ee718630415da1ab329168fc1c10d5d9cf20ad09e4129b2518087231206ca84d7d5ecd5e1a27f66e2fb75d3704b856592fcd43c6d2f7711fa20c0dba1b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,343 @@
1
1
  ## parse-stack-next Changelog
2
2
 
3
+ ### 5.6.0
4
+
5
+ #### Voyage embeddings reach the Atlas endpoint, video, and streamed media
6
+
7
+ - **NEW**: The Voyage provider now targets MongoDB's Atlas Embedding and
8
+ Reranking API in addition to Voyage's own. The two serve the same models
9
+ over an identical wire contract but do not share credentials — an Atlas
10
+ model API key returns 403 from Voyage's host and vice versa. A key carrying
11
+ the Atlas prefix routes to `https://ai.mongodb.com/v1` automatically; pass
12
+ `endpoint: :atlas` or `:voyage` to be explicit, or a `base_url:` to override
13
+ both. A named endpoint that contradicts an explicit `base_url` is rejected
14
+ rather than silently reconciled, so a credential is never sent to a host the
15
+ caller did not intend. `#endpoint` and `#atlas?` report the resolved target.
16
+ - **NEW**: Added `voyage-3.5`, `voyage-3.5-lite`, `voyage-code-2` (1536-dim),
17
+ and `voyage-multimodal-3.5`. Models the Atlas endpoint does not expose —
18
+ `voyage-3`, `voyage-3-lite`, `voyage-4-nano` — are refused at construction
19
+ when that endpoint is active, with an error naming a current replacement;
20
+ they remain valid against Voyage's own API.
21
+ - **NEW**: `Parse::Embeddings::Voyage#embed_video` embeds video through
22
+ `voyage-multimodal-3.5`, the only model that accepts it. Text, image, and
23
+ video vectors share one space, so a stored text vector is comparable against
24
+ a video vector without re-embedding. `#modalities` reports `[:text, :image,
25
+ :video]` for that model and `[:text, :image]` for `voyage-multimodal-3`.
26
+ - **NEW**: `Parse::Embeddings::MediaFile` wraps a local image or video and
27
+ streams it into the request body instead of buffering it. Serializing media
28
+ with `to_json` costs roughly 2.4x the file size resident — raw bytes, the
29
+ 1.33x base64 copy, and the serialized document — which is enough to exhaust
30
+ a small dyno on a single moderate video. `MediaFile` reads only a 16-byte
31
+ header at construction; the payload is then base64-encoded into the socket
32
+ in fixed-size chunks by `Parse::Embeddings::StreamingBody`, so peak memory
33
+ is bounded by the chunk size regardless of file size and nothing spills to
34
+ disk. `Content-Length` is computed exactly so the request avoids chunked
35
+ transfer encoding, and the body replays byte-identically on retry. Passing a
36
+ URL instead keeps the SDK out of the transfer entirely.
37
+ - **FIXED**: Corrected the model dimension table. The entire v4 family
38
+ defaults to 1024 — `voyage-4-large`'s 2048, `voyage-4-lite`'s 512, and
39
+ `voyage-4-nano`'s 256 were recorded as native widths when they are
40
+ Matryoshka options reached only by requesting them. Because the provider
41
+ validates the returned vector width against the declared one, both
42
+ `voyage-4-large` and `voyage-4-lite` raised
43
+ `Parse::Embeddings::InvalidResponseError` on every call. `voyage-4-nano` is
44
+ 1024, and `voyage-finance-2` carries a 32,000-token context rather than
45
+ 16,000.
46
+ - **CHANGED**: The coarse "Matryoshka-capable models" gate is replaced by
47
+ per-model `MODEL_SUPPORTED_DIMENSIONS`. Any width on a model's ladder is now
48
+ accepted — including one wider than its default, which the old rule rejected
49
+ as "exceeds native" — and a width off the ladder is refused with the
50
+ supported set named. `output_dimension` is sent whenever the configured
51
+ width differs from the model's default, so `voyage-4-lite` at 512 and
52
+ `voyage-4-large` at 2048 both work.
53
+ - **FIXED**: A caller-supplied URL can no longer capture a local file's bytes.
54
+ The streamed body previously marked each payload with a sentinel token and
55
+ located it by searching the serialized JSON, so a URL containing that token
56
+ matched first: the file's base64 was spliced into the URL slot and forwarded
57
+ to the provider as a URL to fetch, disclosing local file contents, while the
58
+ intended slot kept the literal token. Request bodies are now assembled
59
+ structurally — each fragment serialized independently and concatenated in
60
+ order — so payloads are placed by position and caller data is never
61
+ searched.
62
+ - **FIXED**: Mixed `image_url` / `image_base64` batches no longer violate
63
+ Voyage's request contract, which requires a single representation per
64
+ request. A mixed batch is split into one request per representation and
65
+ reassembled into the caller's original order, preserving the 1:1 alignment
66
+ between inputs and returned vectors.
67
+ - **FIXED**: Video validation no longer accepts containers the provider
68
+ rejects. MP4 is the only format Voyage supports, and WebM and QuickTime
69
+ payloads are refused by the API, so both are out of the default allowlist.
70
+ An `ftyp` box no longer implies MP4 on its own — QuickTime and the
71
+ audio-only profiles share the ISO base media container — so major brands are
72
+ matched explicitly and an unrecognized brand is refused rather than assumed.
73
+ Apple's audio-only `M4A` brand is excluded, closing a type confusion in
74
+ which an audio file passed as video.
75
+ - **NEW**: `Parse::Embeddings.max_media_bytes` caps streamed media per file,
76
+ defaulting to the 20 MB Voyage documents. Streaming already prevents an
77
+ oversized file from exhausting memory, but the provider still rejects it, so
78
+ failing locally turns a wasted upload into an immediate error. The Voyage
79
+ adapter enforces the 20 MB ceiling independently, so raising the global knob
80
+ for another provider cannot push an oversized payload onto Voyage.
81
+
82
+ #### Breaking
83
+
84
+ - **BREAKING**: The Voyage provider's default model moves from `voyage-3` to
85
+ `voyage-3.5`. `voyage-3` is retired from the Atlas endpoint, so the old
86
+ default made an Atlas key fail at construction whenever no model was named.
87
+ Vectors from the two models are not comparable. **Migration:** code relying
88
+ on the default must pin `model: "voyage-3"` explicitly to keep existing
89
+ embeddings valid, or re-embed against the new default. A `:vector` property
90
+ that declares `model:` is unaffected — the new binding audit catches the
91
+ mismatch before any request rather than letting the two mix silently.
92
+
93
+ #### Vector search no longer returns fewer results than requested
94
+
95
+ - **FIXED**: `$vectorSearch` set its `limit` to `k`, but Atlas applies that
96
+ limit before the SDK's ACL `$match`, `protectedFields` redaction,
97
+ pointer-field filtering, and any caller-supplied `filter` — so a scoped
98
+ caller who could read 2 of the top 10 documents asked for 10 and received 2,
99
+ even when hundreds of readable matches existed further down the ranking.
100
+ The search now requests a wider internal candidate window, applies every
101
+ enforcement layer, and only then trims to `k`. The window is raised only
102
+ when something can actually drop rows, so a master-key call with no filter
103
+ keeps its previous one-for-one cost. A `candidate_limit:` option on
104
+ `VectorSearch.search` and `find_similar` tunes the window for principals
105
+ whose visibility is unusually narrow. This is a mitigation, not a guarantee:
106
+ a sufficiently selective ACL can still exhaust any finite window, which is
107
+ why the attrition counts below exist. HNSW width stays anchored to `k`, so
108
+ the wider window does not widen the ANN search.
109
+ - **NEW**: `VectorSearch.search` emits a `parse.vector_search.search`
110
+ `ActiveSupport::Notifications` event carrying `candidate_limit`,
111
+ `num_candidates`, `post_filter_count`, `post_pointer_count`,
112
+ `returned_count`, `pointer_attrition`, and `underfilled`. The counts are
113
+ named for where they are measured — obtaining a true pre-`$match` count
114
+ would require a `$facet` — so an underfill is observable rather than silent.
115
+
116
+ #### Vector properties are checked against the provider actually registered
117
+
118
+ - **FIXED**: A `:vector` property's `model:` was recorded and never enforced.
119
+ Because models in the same family usually share a width (`voyage-3` and
120
+ `voyage-3.5` are both 1024), swapping the registered provider's model
121
+ silently mixed incomparable vectors into one index — no error, just
122
+ degraded recall, repairable only by re-embedding. `dimensions:` was
123
+ verified, but only against a vector the provider had already returned and
124
+ billed for. Both are now checked by `Parse::Embeddings::BindingAudit`
125
+ before any request is issued, on the managed-write path and the
126
+ query-embedding path alike. The audit runs ahead of the digest short-circuit
127
+ so an unchanged record still surfaces a drifted binding, and it fails closed:
128
+ a provider that cannot report `#model_name` or `#dimensions` is refused
129
+ rather than skipped, since a declaration that cannot be verified is not a
130
+ declaration that has been satisfied.
131
+ - **NEW**: `Parse::Embeddings::BindingAudit.audit_all!` and
132
+ `.audit_all_or_raise!` check every declared binding at once, for a boot-time
133
+ or CI gate rather than waiting for the first save that happens to touch one.
134
+ - **NEW**: `:vector` properties validate `similarity:` against the functions
135
+ Atlas accepts (`euclidean`, `cosine`, `dotProduct`) at declaration time
136
+ instead of surfacing a typo as an index error later.
137
+ - **FIXED**: Hybrid search applies the same candidate window as the plain
138
+ vector search, so opting into hybrid no longer underfills where a straight
139
+ vector search would not. Each branch's limit is applied before ACL
140
+ enforcement and was narrower than the plain path's window.
141
+ - **FIXED**: Hybrid search separates the rows each branch retains for fusion
142
+ from the rows Atlas considers before ACL. Conflating them meant the branch
143
+ limit was passed as the vector branch's `k` and then multiplied a second
144
+ time by the plain search's own window derivation — a hybrid `k: 10` asked
145
+ Atlas for 1,000 rows instead of the intended 100, an explicit
146
+ `candidate_limit: 250` became 2,500 on the client path while staying 250 on
147
+ the native path, and any `k` above 100 produced a branch `k` beyond
148
+ `VectorSearch::MAX_K` that failed outright. The fusion depth is now bounded
149
+ by `MAX_K` and the candidate window by Atlas's 10,000 ceiling, with both
150
+ paths using the same window. A `candidate_limit` outside that range is
151
+ refused rather than clamped, and `vector: { candidate_limit: }` is forwarded
152
+ through `hybrid_search`.
153
+ - **FIXED**: The native `$rankFusion` pipeline trimmed to `k` in a `$limit`
154
+ stage that runs after its ACL `$match`, reintroducing the underfill the
155
+ candidate window exists to prevent. It now limits to the candidate window
156
+ and trims to `k` once enforcement has run.
157
+ - **NEW**: A `parse.vector_search.hybrid` notification reports `method`,
158
+ `branch_depth`, `candidate_window`, `post_filter_count`, `returned_count`,
159
+ and `underfilled` for both fusion paths. `branch_depth` differs by method by
160
+ design: the client path enforces ACL inside each branch and can retain the
161
+ narrower fusion depth, while the native path enforces after `$rankFusion`
162
+ and must retain the full window. The value reported is the one that ran.
163
+ - **NEW**: A `:vector` property wider than the Atlas vectorSearch index cap is
164
+ refused unless it declares `searchable: false`. `Parse::Vector` tolerates up
165
+ to 16384 dimensions while an Atlas index caps at 8192, so such a property
166
+ was previously declarable, storable, and permanently unsearchable, with the
167
+ failure appearing only at query time. The two limits remain distinct — they
168
+ govern storage and indexing respectively — but the combination now has to be
169
+ acknowledged.
170
+ - **NEW**: `searchable: false` makes a `:vector` property genuinely
171
+ storage-only, whether it opted out to clear the index cap or by choice. It
172
+ is excluded from `find_similar` / `hybrid_search` field resolution, refused
173
+ with an explanation when named directly, and rejected by `agent_searchable`
174
+ at class load rather than at an agent's first query.
175
+
176
+ #### Provider protocol
177
+
178
+ - **NEW**: `Parse::Embeddings::Provider#embed_video` joins `#embed_image` in
179
+ the base protocol with the same `NotImplementedError` default, so video is a
180
+ declared capability rather than a Voyage-only method. `#supports_modality?`
181
+ answers the capability question without rescuing.
182
+ - **NEW**: `rake test:contract` runs live, billable provider contract tests
183
+ that pin request routing, native dimensions, the Matryoshka ladder, accepted
184
+ media, size limits, model availability, and response shape. They skip unless
185
+ `VOYAGE_CONTRACT_KEY` is set and are excluded from both `rake test` and
186
+ `rake test:unit`, so no ordinary run becomes billable because a key happens
187
+ to be exported. Probes that assert a refusal issue raw requests rather than
188
+ going through the SDK — a local guard asserted against itself proves nothing
189
+ about the contract it encodes — and distinguish a genuine refusal from an
190
+ authentication, rate-limit, or 5xx failure so infrastructure trouble cannot
191
+ read as a contract verdict. Mocked tests assert what the SDK believes the
192
+ API does and therefore cannot detect provider drift: every dimension and
193
+ media-format correction in this release was invisible to a fully green
194
+ mocked suite.
195
+
196
+ ### Behavior Notes
197
+
198
+ - Audio is not offered by any Voyage model, and neither PDF nor DOCX is
199
+ accepted as a content type. Render document pages to images and embed those;
200
+ the SDK does not perform that conversion.
201
+ - Deterministic result fill under highly selective ACLs would require the
202
+ authorization predicate to run inside the Atlas prefilter rather than after
203
+ it. The post-search `$match` remains the enforcement boundary regardless.
204
+ - `voyage-4-nano` is served by neither hosted endpoint — it is open-weight and
205
+ meant to be self-hosted. It is refused against Voyage's and Atlas's hosts
206
+ with a message pointing at a self-hosted `base_url:` or
207
+ `Parse::Embeddings::LocalHTTP`, and remains usable through either.
208
+
209
+ ### Code Example
210
+
211
+ ```ruby
212
+ # Endpoint inferred from the key prefix — no base_url needed.
213
+ provider = Parse::Embeddings::Voyage.new(
214
+ api_key: ENV.fetch("ATLAS_MODEL_API_KEY"),
215
+ model: "voyage-multimodal-3.5",
216
+ )
217
+ provider.endpoint # => :atlas
218
+ provider.modalities # => [:text, :image, :video]
219
+
220
+ # Local media streams into the request; the bytes are never held in memory.
221
+ provider.embed_image([Parse::Embeddings::MediaFile.image("page.png")])
222
+ provider.embed_video([Parse::Embeddings::MediaFile.video("demo.mp4")])
223
+
224
+ # A URL keeps the SDK out of the transfer entirely — the provider fetches it.
225
+ provider.embed_image(["https://cdn.example.com/page.png"])
226
+ ```
227
+
228
+ ### 5.5.6
229
+
230
+ #### MCP clients now receive the SSE response instead of hanging
231
+
232
+ - **FIXED**: The Streamable HTTP SSE transport framed its events with custom
233
+ event names — `event: progress` for `notifications/progress` and
234
+ `event: response` for the final JSON-RPC response. MCP defines a single SSE
235
+ event type for JSON-RPC traffic, and clients match only the default
236
+ `message` type, so every frame the SDK emitted was silently discarded: tool
237
+ progress never surfaced, and, critically, the terminating response never
238
+ arrived, leaving the client blocked until its own timeout on any streaming
239
+ `tools/call`. All frames on both the request-scoped POST stream and the
240
+ server-to-client GET notification stream now carry `event: message`, and
241
+ clients discriminate from the JSON-RPC envelope (`method` present for a
242
+ notification, `id` plus `result`/`error` for a response) as the protocol
243
+ intends. Deployments that worked around this with a middleware rewriting
244
+ the event name can drop it.
245
+
246
+ ### 5.5.5
247
+
248
+ #### Agent `call_method` runs under the caller's scope, not the master key
249
+
250
+ - **FIXED**: Instance `call_method` resolved its receiver with a bare
251
+ `klass.find` on the master-backed default client, so a scoped agent could
252
+ read — and through a write method, mutate — any object by id. The receiver
253
+ (both the real call and the dry-run existence check) is now fetched through
254
+ the requesting agent's scope: a session scope fetches with the session token
255
+ (Parse Server enforces ACL/CLP), an `acl_user` / `acl_role` scope routes
256
+ mongo-direct (ACL simulation), and a row the scope cannot read fails closed
257
+ to "not found". The method body runs inside the caller's session so its own
258
+ writes inherit the same principal, and an instance write/admin method under
259
+ an `acl_user` / `acl_role` scope — which has no session token to bind — is
260
+ refused rather than run with master authority.
261
+ - **FIXED**: Aggregation pipelines now default-deny joins under an active
262
+ tenant scope. A `$lookup` / `$graphLookup` / `$unionWith` sub-pipeline runs
263
+ in the joined collection's context with no tenant predicate injected, so any
264
+ such join could surface rows from other tenants; all are refused while a
265
+ tenant scope is active. The `$unionWith` bare-string shorthand
266
+ (`{ "$unionWith" => "Class" }`) is now covered by that guard as well as the
267
+ class-allowlist, hidden/underscore, and CLP-`find` gates, which previously
268
+ inspected only the Hash form.
269
+ - **IMPROVED**: Unimplemented agent tools now raise a typed
270
+ `Parse::Agent::NotImplemented` error, and tools without a handler are omitted
271
+ from tool listings.
272
+
273
+ #### Blank or injected credentials no longer escalate to the master key
274
+
275
+ - **FIXED**: An explicitly-supplied blank or whitespace `session_token:` no
276
+ longer falls through to the master key. It now fails closed to an anonymous
277
+ request (master suppressed, no session header) and does not fall back to a
278
+ bound or ambient token. `Parse.with_session` likewise rejects a blank token
279
+ at the source instead of storing a whitespace ambient that the request layer
280
+ would then drop.
281
+ - **FIXED**: The control options `use_master_key` and `session` can no longer
282
+ be set from a string-keyed conditions hash — the "forward a request params
283
+ hash straight into `Query.new` / `where`" pattern. `{"use_master_key" =>
284
+ true}` from untrusted params was an ACL/CLP-bypass mass-assignment; these two
285
+ keys are now honored only when passed as symbols (code-authored), and a
286
+ string form is treated as an ordinary field constraint with a warning.
287
+
288
+ #### Atlas Search and aggregation stay ACL-scoped
289
+
290
+ - **FIXED**: Atlas Search keeps `$search` at pipeline stage 0 while running the
291
+ scoped ACL/CLP enforcement chain (ACL `$match` folded after `$search`,
292
+ protectedFields strip, pointerFields filter, and a protected-field `$expr`
293
+ oracle guard). Query-derived constraints on the builder-block, options, and
294
+ autocomplete paths are converted to MongoDB storage form before the
295
+ post-`$search` `$match`, so a pointer/date/objectId constraint targets the
296
+ correct storage column and no longer fails BSON serialization.
297
+ - **FIXED**: `search_with_stage` rejects a non-`$search` stage and any
298
+ `returnStoredSource`, which could otherwise return only index-stored fields
299
+ (without `_rperm`), be read as public by the ACL match, and leak restricted
300
+ rows.
301
+ - **FIXED**: A scoped `$geoNear` folds the ACL predicate into `$geoNear.query`
302
+ rather than prepending a `$match`, keeping `$geoNear` at stage 0 so the query
303
+ no longer fails under ACL scoping. The fold embeds a copy of the caller's
304
+ existing query so the caller's pipeline is not mutated.
305
+
306
+ #### Outbound fetches and the shipped CLI are hardened
307
+
308
+ - **FIXED**: `parse-console --url` now parses and scheme-validates its argument
309
+ (HTTP(S) only) before fetching, instead of passing it to `Kernel#open` —
310
+ closing a path where a `|command` argument would be executed as a subprocess.
311
+ - **IMPROVED**: The Cohere reranker `base_url` is validated to reject
312
+ credentials embedded in the URL and plaintext HTTP to non-loopback hosts.
313
+ - **IMPROVED**: Remote file/image fetches enforce a streaming size cap that
314
+ aborts mid-download; the per-call `max_bytes:` ceiling is validated as a
315
+ positive integer, so a zero, negative, or non-numeric value is refused up
316
+ front rather than silently rejecting every response.
317
+
318
+ #### Redaction, locking, and API ergonomics
319
+
320
+ - **IMPROVED**: Query-string credential redaction for logging and request
321
+ profiling is now a single shared implementation, so the two cannot drift. It
322
+ redacts the value of any credential-bearing parameter name (matched by a
323
+ generic rule with a safe-list, and aware of percent-encoded names) and uses
324
+ possessive-quantifier matching so it stays linear on pathological URLs.
325
+ - **FIXED**: The in-process fallback mutex registry (used when no shared lock
326
+ store is configured) is bounded, and its eviction can no longer reclaim a
327
+ mutex a caller is about to lock — a pending-acquirer reservation prevents two
328
+ callers from getting distinct mutexes for the same key.
329
+ - **IMPROVED**: `call_function` and `trigger_job` accept request options as
330
+ bare keyword arguments (for example `call_function("f", {}, session_token:
331
+ t)`), merged with the explicit `opts:` hash for back-compat.
332
+
333
+ #### Packaging, CI, and docs
334
+
335
+ - **CHANGED**: The shipped-gem file list was narrowed to an allowlist of the
336
+ library, binaries, docs, examples, and standard metadata.
337
+ - **CHANGED**: The SHA-pinned `ruby/setup-ruby` GitHub action was bumped to
338
+ `v1.318.0` across the CI, docs, and release workflows.
339
+ - **IMPROVED**: README, test-server, and guide updates, plus YARD styling.
340
+
3
341
  ### 5.5.4
4
342
 
5
343
  #### Dependency updates
@@ -1183,10 +1183,21 @@ module Parse
1183
1183
  # Wire format for each SSE event (note: trailing blank line is required
1184
1184
  # by the SSE spec):
1185
1185
  #
1186
- # event: progress\n
1186
+ # event: message\n
1187
1187
  # data: <json>\n
1188
1188
  # \n
1189
1189
  #
1190
+ # EVERY frame — progress notifications, list-changed notifications,
1191
+ # and the final JSON-RPC response alike — carries the event name
1192
+ # `message`. MCP Streamable HTTP defines exactly one SSE event type
1193
+ # for JSON-RPC traffic; clients discriminate by inspecting the
1194
+ # envelope (`method` present => notification, `id` + `result`/`error`
1195
+ # => response), NOT by the SSE event name. Earlier releases emitted
1196
+ # `event: progress` and `event: response`, which real MCP clients
1197
+ # silently discard — they match only the default `message` type — so
1198
+ # the final response never arrived and the call appeared to hang.
1199
+ # Do not reintroduce custom event names.
1200
+ #
1190
1201
  # @api private
1191
1202
  class SSEBody
1192
1203
  # Sentinel pushed to the queue when the worker is done.
@@ -1546,6 +1557,9 @@ module Parse
1546
1557
  # The `total` field is omitted (rather than nil) so the wire
1547
1558
  # shape matches the spec's optional-field convention.
1548
1559
  #
1560
+ # Emitted as `event: message` — see the SSEBody class docs. The
1561
+ # payload's `method` is what marks it as progress.
1562
+ #
1549
1563
  # @param elapsed [Float] seconds elapsed since the stream started.
1550
1564
  # @return [String] SSE event string (includes trailing blank line).
1551
1565
  def build_progress_event(elapsed)
@@ -1557,15 +1571,14 @@ module Parse
1557
1571
  "progress" => elapsed,
1558
1572
  },
1559
1573
  })
1560
- "event: progress\ndata: #{data}\n\n"
1574
+ "event: message\ndata: #{data}\n\n"
1561
1575
  end
1562
1576
 
1563
1577
  # Format a `notifications/tools/list_changed` or
1564
1578
  # `notifications/prompts/list_changed` SSE event. Both
1565
1579
  # notifications have no `params` — the wire shape is just the
1566
- # JSON-RPC envelope with `method` set. SSE event name is
1567
- # "message" since this is not a progress notification (the
1568
- # progress event name is reserved for progress notifications).
1580
+ # JSON-RPC envelope with `method` set. Emitted as `event: message`,
1581
+ # like every other frame on this stream.
1569
1582
  #
1570
1583
  # @param method [String] full MCP method string.
1571
1584
  # @return [String] SSE event string (includes trailing blank line).
@@ -1599,7 +1612,7 @@ module Parse
1599
1612
  "method" => "notifications/progress",
1600
1613
  "params" => params,
1601
1614
  })
1602
- "event: progress\ndata: #{data}\n\n"
1615
+ "event: message\ndata: #{data}\n\n"
1603
1616
  end
1604
1617
 
1605
1618
  # Build the callback the dispatcher block passes into
@@ -1631,12 +1644,17 @@ module Parse
1631
1644
  end
1632
1645
  end
1633
1646
 
1634
- # Format the final `response` SSE event.
1647
+ # Format the final JSON-RPC response SSE event.
1648
+ #
1649
+ # Emitted as `event: message` (NOT `event: response`) — an MCP
1650
+ # client matching only the default `message` type would otherwise
1651
+ # discard the response and block until its own timeout. The
1652
+ # envelope's `id` + `result`/`error` is what marks it final.
1635
1653
  #
1636
1654
  # @param body [Hash] JSON-RPC response envelope.
1637
1655
  # @return [String] SSE event string (includes trailing blank line).
1638
1656
  def build_response_event(body)
1639
- "event: response\ndata: #{JSON.generate(body)}\n\n"
1657
+ "event: message\ndata: #{JSON.generate(body)}\n\n"
1640
1658
  end
1641
1659
 
1642
1660
  # Build an internal-error JSON-RPC envelope (id may be nil at this layer).
@@ -1755,9 +1773,9 @@ module Parse
1755
1773
  end
1756
1774
  end
1757
1775
 
1758
- # SSE wire form for a server→client notification. Event name "message"
1759
- # (not "progress"/"response", which are reserved for the request-scoped
1760
- # SSE path).
1776
+ # SSE wire form for a server→client notification. Event name
1777
+ # "message" the single event type MCP Streamable HTTP defines for
1778
+ # JSON-RPC traffic, matching the request-scoped SSE path.
1761
1779
  def format_event(notification)
1762
1780
  "event: message\ndata: #{JSON.generate(notification)}\n\n"
1763
1781
  end
@@ -634,6 +634,16 @@ module Parse
634
634
  "agent_searchable field: :#{field_sym} is not a declared :vector property " \
635
635
  "on #{parse_class_name} (declared: #{vector_properties.keys.inspect})."
636
636
  end
637
+ # A storage-only property is not eligible for search, so the
638
+ # tool this registers could never run. Refuse at class load
639
+ # rather than at the first agent query.
640
+ if respond_to?(:vector_properties) &&
641
+ vector_properties.dig(field_sym, :searchable) == false
642
+ raise ArgumentError,
643
+ "agent_searchable field: :#{field_sym} on #{parse_class_name} is declared " \
644
+ "`searchable: false` (storage-only, not eligible for search) and cannot " \
645
+ "back a search tool."
646
+ end
637
647
  filters = Array(filter_fields).map(&:to_sym)
638
648
  @agent_searchable_field = field_sym
639
649
  @agent_searchable_filter_fields = filters