@quantod/qq 1.1.19 → 1.2.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.
@@ -21,7 +21,7 @@ Stages are just labels. The pipeline has no built-in notion of "first" or "last"
21
21
  Items have:
22
22
  - `id` — the deduplication key. Pushing an item with an existing id fails.
23
23
  - `stage` — the current state
24
- - `payload` — item data; append-only string by default; YAML by convention.
24
+ - `payload` — item data; a structured JSON object (or array). Merged with json_patch by default; replaced when `replace: true`.
25
25
  - `seq` — the claim token, returned by `claim`, required to `release`
26
26
  - `priority` — float, higher = claimed first (default 0.0)
27
27
 
@@ -34,12 +34,12 @@ make_pipeline → create a pipeline, get its name, once per setup
34
34
  push (N times) → load items into a stage
35
35
  claim → atomically lock one item (returns seq + payload)
36
36
  ... process the claimed item ...
37
- release → unlock item, move to next stage, append payload
37
+ release → unlock item, move to next stage, merge payload
38
38
  status → monitor progress
39
39
  unstick → recover stuck/lost items
40
40
  ```
41
41
 
42
- **Orchestrator** creates the pipeline, pushes items, spawns subagents, monitors with `status`, calls `batch_read` to collect results, calls `unstick` after.
42
+ **Orchestrator** creates the pipeline, pushes items, spawns subagents, monitors with `status`, calls `query` to collect results, calls `unstick` after.
43
43
 
44
44
  **Subagent** claims one item, processes it, releases to the next stage, repeats until "no items to claim" — then stops and reports back. Never process an item before claiming it: another agent could claim and modify it concurrently.
45
45
 
@@ -50,9 +50,9 @@ unstick → recover stuck/lost items
50
50
  Orchestrator example:
51
51
  ```
52
52
  make_pipeline → "0529_a3f9c2" (store this as Q)
53
- push Q/pending "url-1" payload: "url: https://a.com"
54
- push Q/pending "url-2" payload: "url: https://b.com"
55
- push Q/pending "url-3" payload: "url: https://c.com"
53
+ push Q/pending "url-1" payload: {url: "https://a.com"}
54
+ push Q/pending "url-2" payload: {url: "https://b.com"}
55
+ push Q/pending "url-3" payload: {url: "https://c.com"}
56
56
  ```
57
57
 
58
58
  Spawn subagents with instructions like:
@@ -98,8 +98,7 @@ Parameters:
98
98
  - `pipeline` (string) — pipeline name
99
99
  - `stage` (string, optional) — destination stage. Supports sub-paths: `enriched/shortlisted`. Defaults to empty string if omitted.
100
100
  - `id` (string, optional) — stable unique identifier. Omit to use auto-generated seq number.
101
- - `payload` (string, optional) — payload string
102
- - `payloadFormat` (`yaml` | `json` | `text`, optional, default `yaml`) — `yaml`: validate as YAML. `json`: validate as JSON and minify to one line (appended payloads form valid JSONL). `text`: no validation.
101
+ - `payload` (object, optional) — structured payload object. Stored as SQLite JSONB. Any JSON-serializable object or array.
103
102
  - `priority` (number, optional) — float, default 0.0. Higher = claimed first. Only set when meaningful.
104
103
 
105
104
  Returns:
@@ -111,8 +110,6 @@ id: url-1
111
110
 
112
111
  Errors:
113
112
  - `error: item <id> already in the pipeline` — duplicate id; item already exists
114
- - `error: payload is not valid YAML: <detail>` — `payloadFormat` is `yaml` (default) and payload fails YAML parse
115
- - `error: payload is not valid JSON: <detail>` — `payloadFormat` is `json` and payload fails JSON parse
116
113
 
117
114
  ---
118
115
 
@@ -135,7 +132,7 @@ priority: 0
135
132
  claimed: true
136
133
  created: 2025-05-28T14:30:00.000Z
137
134
  last_modified: 2025-05-28T14:30:00.000Z
138
- payload: |
135
+ payload:
139
136
  url: https://example.com/product/42
140
137
  ```
141
138
 
@@ -156,9 +153,8 @@ Parameters:
156
153
  - `pipeline` (string)
157
154
  - `seq` (number) — the claim token from `claim`
158
155
  - `target` (string, optional) — destination stage. **Always specify when moving the item.** Omit to release back to the same stage.
159
- - `payload` (string, optional) — payload string to append (or replace)
160
- - `payloadFormat` (`yaml` | `json` | `text`, optional, default `yaml`) same as `push`
161
- - `replace` (boolean, optional) — replace payload entirely instead of appending. Use when treating payload as a structured object.
156
+ - `payload` (object, optional) — structured payload object to merge into the existing payload
157
+ - `replace` (boolean, optional) replace payload entirely instead of merging. Use when you want to discard the previous payload.
162
158
  - `priority` (number, optional) — update the item's priority
163
159
 
164
160
  Returns:
@@ -169,10 +165,8 @@ ok: true
169
165
  Errors:
170
166
  - `error: claim on seq <n> expired` — seq no longer exists; discard your work
171
167
  - `error: item seq <n> not claimed` — items should be claimed before releasing - likely bug
172
- - `error: payload is not valid YAML: <detail>` — `payloadFormat` is `yaml` (default) and payload fails YAML parse
173
- - `error: payload is not valid JSON: <detail>` — `payloadFormat` is `json` and payload fails JSON parse
174
168
 
175
- **Payload is append-only history by default.** Each stage appends its output resulting duplicate YAML fields are intentional. Use `replace: true` only when treating payload as a structured object you own.
169
+ **Payload merge uses json_patch (RFC 7396) by default.** When you pass a `payload` object, it is merged into the existing payload using RFC 7396 semantics: keys present in the patch are set on the target (recursively for nested objects); keys set to `null` in the patch are **removed** from the target; keys absent from the patch are left unchanged. Omitting `payload` leaves the existing payload unchanged. Use `replace: true` to discard the existing payload entirely.
176
170
 
177
171
  ---
178
172
 
@@ -189,47 +183,36 @@ description: Scrape product pages and extract pricing
189
183
  stats:
190
184
  /*: [23, 3]
191
185
  done: [7, 0]
192
- failed: [6, 0]
193
- pending: [10, 3]
194
- ```
195
-
196
- Each value is `[total, claimed]` — total item count and how many are currently claimed (being processed). `/*` is the pipeline-level aggregate across all stages. `description` is `null` if none was set.
197
-
198
- Aggregate vs. direct entries: a key ending in `/*` is an aggregate — it sums all items in that prefix and its sub-stages. A key without `/*` is a direct count — only items whose stage exactly matches that key. When a stage has sub-stages, both appear:
199
-
200
- ```yaml
201
- stats:
202
- /*: [23, 3]
203
- done: [7, 0]
204
- enriched: [3, 1] # items directly in 'enriched'
205
- enriched/*: [10, 2] # aggregate: enriched + enriched/shortlisted
186
+ enriched: [3, 1]
187
+ enriched/*: [10, 2]
206
188
  enriched/shortlisted: [7, 1]
207
189
  failed: [6, 0]
208
190
  pending: [10, 3]
209
191
  ```
210
192
 
211
- A stage that has no sub-stages appears only as a direct entry (no `/*` variant).
193
+ Each value is `[total, claimed]` total item count and how many are currently claimed (being processed). `/*` is the pipeline-level aggregate across all stages. A key ending in `*` is an aggregate — it sums all items in that prefix and its sub-stages.
212
194
 
213
195
  Errors:
214
196
  - `error: pipeline <name> not found`
215
197
 
216
198
  ---
217
199
 
218
- ### batch_read
200
+ ### query
219
201
 
220
202
  Reads items without claiming them. For inspection, data collection, and monitoring.
221
203
 
222
204
  Parameters:
223
205
  - `pipeline` (string)
224
206
  - `stage` (string, optional) — supports globs. Defaults to `*`
225
- - `includePayload` (boolean, optional) — include payload in results
226
- - `claimed` (boolean, optional) — filter by claimed status. Default: unclaimed
207
+ - `select` (`"*"` | string[], optional) — which payload fields to return. `"*"` returns the full payload; an array of dot-notation paths (e.g. `["status", "meta.author"]`) returns only those fields; omit to return no payload.
208
+ - `filter` (object, optional) — MongoDB-style filter on payload fields. Keys are dot-notation paths into the payload. Values are either a literal (equality match) or an operator object. Supported operators: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in` (array), `$exists` (boolean). Example: `{ "status": "active", "score": { "$gt": 80 } }`.
209
+ - `claimed` (boolean, optional) — filter by claimed status
227
210
  - `ids` (string[], optional)
228
211
  - `createdAfter`, `createdBefore`, `modifiedAfter` (string | number) — epoch ms, ISO 8601 (`"2025-05-28"`, `"2025-05-28T14:30:00Z"`), or relative (`"-2h"`, `"-1d"`, `"-30m"`, `"-1w"`)
229
212
  - `limit`, `offset` (number)
230
213
  - `strategy` (`priority` | `fifo` | `lifo` | `random`, optional, default `fifo`) — sort order. Same semantics as `claim`: `priority` highest first then lowest seq, `fifo` lowest seq first, `lifo` highest seq first, `random` random order.
231
214
 
232
- Returns a YAML list of items. `payload` is omitted unless `includePayload: true`. Returns `[]` when nothing matches.
215
+ Returns a YAML list of items. `payload` is included only when `select` is provided. Returns `[]` when nothing matches.
233
216
 
234
217
  ```yaml
235
218
  - id: url-1
@@ -239,6 +222,9 @@ Returns a YAML list of items. `payload` is omitted unless `includePayload: true`
239
222
  priority: 0
240
223
  created: 2025-05-28T14:30:00.000Z
241
224
  last_modified: 2025-05-28T14:30:01.000Z
225
+ payload:
226
+ url: https://example.com
227
+ score: 92
242
228
  - id: url-2
243
229
  stage: done
244
230
  claimed: false
@@ -246,6 +232,9 @@ Returns a YAML list of items. `payload` is omitted unless `includePayload: true`
246
232
  priority: 0
247
233
  created: 2025-05-28T14:30:00.001Z
248
234
  last_modified: 2025-05-28T14:30:02.000Z
235
+ payload:
236
+ url: https://other.com
237
+ score: 47
249
238
  ```
250
239
 
251
240
  ---
@@ -296,31 +285,32 @@ Returns a code snippet to inline at the top of your script. The snippet uses onl
296
285
  QQ.push(pipeline, stage, id?, payload?, opts?) → Promise<{ id: string } | { error: string }>
297
286
  QQ.claim(pipeline, stage, opts?) → Promise<Item | { error: string }>
298
287
  QQ.release(pipeline, seq, opts?) → Promise<{ ok: true } | { error: string }>
299
- QQ.batch_read(pipeline, stage, opts?) → Promise<Item[] | { error: string }>
288
+ QQ.query(pipeline, stage, opts?) → Promise<Item[] | { error: string }>
300
289
  QQ.status(pipeline) → Promise<{ description: string|null, stats: Record<string, [number,number]> } | { error: string }>
301
290
  ```
302
291
 
303
- - `push` opts: `priority` (number), `payloadFormat` (`"yaml"` | `"json"` | `"text"`)
292
+ - `push` opts: `priority` (number)
304
293
  - `claim` opts: `id` (string — claim a specific item), `strategy` (`"priority"` | `"fifo"` | `"lifo"` | `"random"`)
305
- - `release` opts: `target` (string), `payload` (string), `replace` (boolean), `priority` (number), `payloadFormat`
306
- - `batch_read` opts: `includePayload` (boolean), `claimed` (boolean), `ids` (string, comma-separated), `createdAfter`, `createdBefore`, `modifiedAfter` (epoch ms, ISO 8601, or relative e.g. `"-2h"`), `limit`, `offset` (number), `strategy` (`"priority"` | `"fifo"` | `"lifo"` | `"random"`)
294
+ - `release` opts: `target` (string), `payload` (object), `replace` (boolean), `priority` (number)
295
+ - `query` opts: `select` (`"*"` or string[] of dot-notation paths), `filter` (object — MongoDB-style payload filter), `claimed` (boolean), `ids` (string, comma-separated), `createdAfter`, `createdBefore`, `modifiedAfter` (epoch ms, ISO 8601, or relative e.g. `"-2h"`), `limit`, `offset` (number), `strategy` (`"priority"` | `"fifo"` | `"lifo"` | `"random"`)
307
296
 
308
297
  All functions return `{ error: string }` on failure — never throw (except `BridgeError` for network connection failures).
309
298
 
310
- Item shape (returned by `claim` and `batch_read`):
299
+ Item shape (returned by `claim` and `query`):
311
300
  ```
312
301
  { id: string, stage: string, seq: number, priority: number, claimed: boolean,
313
- created: number, last_modified: number, payload?: string | null }
302
+ created: number, last_modified: number, payload?: object | null }
314
303
  ```
315
304
 
316
- `claim` always includes `payload`. `batch_read` omits `payload` unless `includePayload: true`.
305
+ `claim` always includes `payload`. `query` omits `payload` unless `select` is provided.
306
+ `payload` is a parsed JavaScript object (or null) — never a string.
317
307
 
318
308
  **Python signatures:**
319
309
  ```
320
310
  qq_push(pipeline, stage, id=None, payload=None, **opts) → { 'id': str } | { 'error': str }
321
311
  qq_claim(pipeline, stage, **opts) → dict | { 'error': str }
322
312
  qq_release(pipeline, seq, **opts) → { 'ok': True } | { 'error': str }
323
- qq_batch_read(pipeline, stage, **opts) → list[dict] | { 'error': str }
313
+ qq_query(pipeline, stage, **opts) → list[dict] | { 'error': str }
324
314
  qq_status(pipeline) → { 'description': str|None, 'stats': dict } | { 'error': str }
325
315
  ```
326
316
 
@@ -328,9 +318,9 @@ All Python functions return `{ 'error': str }` on failure — never raise (excep
328
318
 
329
319
  ---
330
320
 
331
- ### load_file
321
+ ### upload_file
332
322
 
333
- Loads multiple items from a file on the host filesystem into a pipeline. Each record in the file becomes one pipeline item.
323
+ Uploads multiple items from a file on the host filesystem into a pipeline. Each record in the file becomes one pipeline item.
334
324
 
335
325
  **Supported formats:** JSONL (`.jsonl`, `.ndjson`), JSON array (`.json`), YAML array (`.yaml`, `.yml`), CSV (`.csv`). Format is detected from the file extension.
336
326
 
@@ -341,7 +331,7 @@ Parameters:
341
331
  - `pipeline` (string) — pipeline to push records into
342
332
  - `stage` (string, optional) — default stage for records that do not specify their own. Defaults to empty string.
343
333
  - `deleteAfter` (boolean, optional) — delete the file after loading. Default: true.
344
- - `duplicates` (`ignore` | `append` | `replace`, optional, default `ignore`) — how to handle records whose `id` already exists in the pipeline. `ignore`: skip silently (existing item unchanged). `append`: append the new record's payload to the existing item's payload and bump `seq` (invalidates any live claim). `replace`: replace the existing item's payload entirely and bump `seq`.
334
+ - `duplicates` (`ignore` | `append` | `replace`, optional, default `ignore`) — how to handle records whose `id` already exists in the pipeline. `ignore`: skip silently (existing item unchanged). `append`: merge the new record's payload into the existing payload using json_patch, bump `seq` (invalidates any live claim). `replace`: replace the existing item's payload entirely and bump `seq`.
345
335
 
346
336
  **⚠ Avoid `append` and `replace` in production pipelines.** Both modes write to items outside the normal `claim → process → release` cycle, bypassing QQ's concurrency control. Any agent currently holding a claim on an affected item will find its `seq` token expired and lose its work. `replace` is especially destructive — it silently discards the existing payload with no recovery path. The correct way to update an item's payload is to `claim` it, process it, and `release` it with new content. Use `append`/`replace` only for offline data preparation before a pipeline has active workers.
347
337
 
@@ -349,14 +339,18 @@ Parameters:
349
339
  ```yaml
350
340
  id: item-1
351
341
  stage: pending
352
- payload: "any string"
342
+ payload:
343
+ url: https://example.com
344
+ score: 42
353
345
  priority: 0.0
354
346
  ```
355
347
  - `id` — omit for auto-generated id
356
348
  - `stage` — overrides the `stage` parameter for this record
357
- - `payload` — treated as an opaque string; non-string values are YAML-serialized
349
+ - `payload` — structured object. String values are parsed as YAML for backward compatibility.
358
350
  - `priority` — float, default 0.0
359
351
 
352
+ **CSV format**: for CSV files, all columns except `id`, `stage`, and `priority` become the payload object. A CSV row `id,url,title` with values `a,https://x.com,Hello` produces payload `{url: "https://x.com", title: "Hello"}`.
353
+
360
354
  **Returns** YAML with a summary of counts and a per-item outcome list:
361
355
  ```yaml
362
356
  summary:
@@ -393,24 +387,22 @@ Errors:
393
387
 
394
388
  ---
395
389
 
396
- ### batch_write
390
+ ### upload_data
397
391
 
398
- Same as `load_file` but accepts data inline instead of reading from a file. Use when the records are already in memory or generated on the fly.
392
+ Same as `upload_file` but accepts data inline instead of reading from a file. Use when the records are already in memory or generated on the fly.
399
393
 
400
394
  Parameters:
401
395
  - `data` (string) — the raw record data
402
396
  - `format` (`jsonl` | `json` | `yaml` | `csv`) — data format (explicit, no file extension to infer from)
403
397
  - `pipeline` (string)
404
398
  - `stage` (string, optional) — default stage. Defaults to empty string.
405
- - `duplicates` (`ignore` | `append` | `replace`, optional, default `ignore`) — same semantics as `load_file`. The same warning applies: prefer `claim → process → release` over `append`/`replace` whenever workers may be active.
399
+ - `duplicates` (`ignore` | `append` | `replace`, optional, default `ignore`) — same semantics as `upload_file`. The same warning applies: prefer `claim → process → release` over `append`/`replace` whenever workers may be active.
406
400
 
407
- Returns the same shape as `load_file` (`summary` + `items`), without the `deleteAfter` behaviour.
401
+ Returns the same shape as `upload_file` (`summary` + `items`), without the `deleteAfter` behaviour.
408
402
 
409
403
  Errors:
410
404
  - `error: <message>` — malformed data. Entire operation aborted; no records are pushed.
411
405
 
412
- ---
413
-
414
406
  ## SDK Usage
415
407
 
416
408
  Use the SDK when you need to run code that processes pipeline items programmatically — loops, transforms, bulk operations. It is faster and more efficient than calling MCP tools one at a time for batch work.
@@ -432,25 +424,31 @@ Use the SDK when you need to run code that processes pipeline items programmatic
432
424
 
433
425
  const pipeline = '0529_a3f9c2';
434
426
 
435
- // Bulk push — payload is a YAML string
427
+ // Bulk push — payload is a plain object
436
428
  for (const url of urls) {
437
- await QQ.push(pipeline, 'pending', url, `url: ${url}`);
429
+ await QQ.push(pipeline, 'pending', url, { url });
438
430
  }
439
431
 
440
432
  // Claim loop — check result.error, not null
441
433
  while (true) {
442
434
  const item = await QQ.claim(pipeline, 'pending');
443
435
  if (item.error) break; // "no items to claim" or other error
444
- const result = await processItem(item.payload);
445
- await QQ.release(pipeline, item.seq, { target: 'done', payload: result });
436
+ const result = await processItem(item.payload); // item.payload is already a parsed object
437
+ await QQ.release(pipeline, item.seq, { target: 'done', payload: { result } });
446
438
  }
447
439
 
448
440
  // Collect results with payloads
449
- const done = await QQ.batch_read(pipeline, 'done', { includePayload: true });
441
+ const done = await QQ.query(pipeline, 'done', { select: '*' });
450
442
  for (const item of done) {
451
- console.log(item.id, item.payload);
443
+ console.log(item.id, item.payload); // item.payload is a parsed object
452
444
  }
453
445
 
446
+ // Filter by payload fields and select only specific fields
447
+ const highScores = await QQ.query(pipeline, 'done', {
448
+ filter: { score: { $gt: 80 }, status: 'active' },
449
+ select: ['url', 'score'],
450
+ });
451
+
454
452
  // Monitor progress
455
453
  const s = await QQ.status(pipeline);
456
454
  console.log(`total=${s.stats['/*'][0]} pending=${s.stats.pending?.[0] ?? 0}`);
@@ -466,34 +464,32 @@ while (true) {
466
464
  if (item.error) break;
467
465
  try {
468
466
  const result = await processItem(item.payload);
469
- await QQ.release(pipeline, item.seq, { target: 'done', payload: `result: ${result}` });
467
+ await QQ.release(pipeline, item.seq, { target: 'done', payload: { result } });
470
468
  } catch (e) {
471
- await QQ.release(pipeline, item.seq, { target: 'failed', payload: `error: ${e.message}` });
469
+ await QQ.release(pipeline, item.seq, { target: 'failed', payload: { error: e.message } });
472
470
  }
473
471
  }
474
472
  ```
475
473
 
476
- ### Structured JSON payloads
474
+ ### Working with structured payloads
477
475
 
478
- Use `payloadFormat: "json"` when payload is a JSON object. Appended payloads form valid JSONL; `replace: true` treats it as a single mutable object:
476
+ Payloads are plain objects. `release` merges them by default (json_patch); `replace: true` overwrites:
479
477
 
480
478
  ```javascript
481
- // Push with JSON payload
482
- await QQ.push(pipeline, 'pending', 'item-1', JSON.stringify({ url: 'https://a.com', score: 0 }),
483
- { payloadFormat: 'json' });
479
+ // Push with a structured payload object
480
+ await QQ.push(pipeline, 'pending', 'item-1', { url: 'https://a.com', score: 0 });
484
481
 
485
- // Update a structured payload: parse modify → replace
482
+ // Claim returns payload as a parsed object no JSON.parse needed
486
483
  const item = await QQ.claim(pipeline, 'pending');
487
484
  if (!item.error) {
488
- const data = JSON.parse(item.payload);
489
- data.score = await scoreItem(data.url);
490
- await QQ.release(pipeline, item.seq, {
491
- target: 'done',
492
- payload: JSON.stringify(data),
493
- payloadFormat: 'json',
494
- replace: true,
495
- });
485
+ const score = await scoreItem(item.payload.url);
486
+ // Merge new fields into existing payload (json_patch)
487
+ await QQ.release(pipeline, item.seq, { target: 'done', payload: { score } });
488
+ // Result: { url: 'https://a.com', score: <value> }
496
489
  }
490
+
491
+ // Replace entire payload (discard existing)
492
+ await QQ.release(pipeline, item.seq, { payload: { rebuilt: true }, replace: true });
497
493
  ```
498
494
 
499
495
  ### Python example
@@ -503,21 +499,26 @@ if (!item.error) {
503
499
 
504
500
  pipeline = "0529_a3f9c2"
505
501
 
506
- # Bulk push
502
+ # Bulk push — payload is a dict
507
503
  for url in urls:
508
- qq_push(pipeline, "pending", url, f"url: {url}")
504
+ qq_push(pipeline, "pending", url, {"url": url})
509
505
 
510
- # Claim loop
506
+ # Claim loop — item["payload"] is already a parsed dict
511
507
  while True:
512
508
  item = qq_claim(pipeline, "pending")
513
509
  if "error" in item: # "no items to claim" or other error
514
510
  break
515
- result = process_item(item["payload"])
516
- qq_release(pipeline, item["seq"], target="done", payload=result)
511
+ result = process_item(item["payload"]) # payload is a dict
512
+ qq_release(pipeline, item["seq"], target="done", payload={"result": result})
517
513
 
518
- items = qq_batch_read(pipeline, "done", includePayload=True)
514
+ items = qq_query(pipeline, "done", select="*")
519
515
  for item in items:
520
- print(item["id"], item["payload"])
516
+ print(item["id"], item["payload"]) # payload is a dict
517
+
518
+ # Filter by payload fields; select only specific fields
519
+ high_scores = qq_query(pipeline, "done",
520
+ filter={"score": {"$gt": 80}, "status": "active"},
521
+ select=["url", "score"])
521
522
 
522
523
  # Monitor
523
524
  s = qq_status(pipeline)
@@ -535,39 +536,41 @@ This happens when the QQ MCP server restarted (e.g. Claude Desktop was restarted
535
536
 
536
537
  ---
537
538
 
538
- ## YAML Payload Conventions
539
+ ## Structured Payload and Merge Semantics
539
540
 
540
- Payload is a YAML string appended to existing content by default. The result is a YAML document where fields may appear multiple times — this is intentional. Each stage's output appears in sequence, creating a trace of the item's journey.
541
+ Payloads are structured JSON objects (or arrays), stored as SQLite JSONB. Plain text is not allowed.
542
+
543
+ **Default merge — json_patch (RFC 7396):** When `release` or `upload_data append` receives a payload, it is merged into the existing payload using RFC 7396 semantics:
544
+ - Keys in the patch with a non-null value are set on the target (recursively for nested objects)
545
+ - Keys in the patch set to `null` are **removed** from the target
546
+ - Keys absent from the patch are left unchanged
541
547
 
542
- ```yaml
543
- # After push:
544
- url: https://example.com/product/42
545
-
546
- # After first subagent appends:
547
- url: https://example.com/product/42
548
- name: Widget Pro
549
- price: 29.99
550
- available: true
551
-
552
- # After second subagent appends (review):
553
- url: https://example.com/product/42
554
- name: Widget Pro
555
- price: 29.99
556
- available: true
557
- review_reason: price seems anomalous
558
548
  ```
549
+ # Existing: { url: "https://x.com", score: 0, tags: ["a"], draft: true }
550
+ # Patch: { score: 42, extra: "new", draft: null }
551
+ # Result: { url: "https://x.com", score: 42, tags: ["a"], extra: "new" }
552
+ # (draft removed because patch set it to null)
553
+ ```
554
+
555
+ **Replace:** Pass `replace: true` to discard the existing payload entirely and store only the new value.
556
+
557
+ **No payload:** Omit `payload` from `release` to leave the existing payload unchanged (useful when only moving stage or updating priority).
559
558
 
560
- Use `replace: true` only when you own the payload as a structured object (e.g., accumulating messages in a chat pipeline). To update a structured payload: claim, parse with yaml.load, modify, release with replace.
559
+ **Null payload:** An item pushed with no `payload` argument stores SQL NULL `claim` and `query` return it as `null`.
560
+
561
+ **CLI and SDK string input:** The CLI (`--payload`) and `payloadFormat` option accept YAML or JSON strings that are parsed before storage. The `text` format has been removed — all inputs must be valid YAML or JSON.
561
562
 
562
563
  ---
563
564
 
564
565
  ## Globs, Filters, Priority
565
566
 
566
- **Stage globs**: `*` matches any single segment, `enriched/*` matches sub-stages. Use in `claim` and `batch_read` to work across multiple stages.
567
+ **Stage globs**: `*` matches any single segment, `enriched/*` matches sub-stages. Use in `claim` and `query` to work across multiple stages.
567
568
 
568
569
  **Priority**: float, default 0.0. Higher value = claimed first within the same stage. Use only when meaningful (e.g., score, confidence). Most pipelines don't need priority.
569
570
 
570
- **Filters in batch_read / unstick**: `claimed`, `ids`, `createdAfter`, `createdBefore`, `modifiedAfter` (epoch ms, ISO 8601, or relative: `"-2h"`, `"-1d"`, `"-30m"`, `"-1w"`), `limit`, `offset`. **batch_read** also accepts `strategy` (`priority` | `fifo` | `lifo` | `random`, default `fifo`).
571
+ **Filters in query / unstick**: `claimed`, `ids`, `createdAfter`, `createdBefore`, `modifiedAfter` (epoch ms, ISO 8601, or relative: `"-2h"`, `"-1d"`, `"-30m"`, `"-1w"`), `limit`, `offset`. **query** also accepts `strategy` (`priority` | `fifo` | `lifo` | `random`, default `fifo`), `filter` (MongoDB-style payload filter), and `select` (`"*"` or array of dot-notation field paths).
572
+
573
+ **Payload filter operators** (`filter` in `query`): `{ "field": value }` (equality), `{ "field": { "$gt": n } }`, `$gte`, `$lt`, `$lte`, `$ne`, `{ "field": { "$in": [a, b] } }`, `{ "field": { "$exists": true } }`. Nested fields use dot notation: `"meta.author"`. All conditions are ANDed together.
571
574
 
572
575
  ---
573
576
 
@@ -13,7 +13,7 @@ const QQ = (() => {
13
13
  push: (pipeline, stage, id, payload, opts = {}) => _req('POST', \`/pipelines/\${pipeline}/push\`, { stage, id, payload, ...opts }),
14
14
  claim: (pipeline, stage, opts = {}) => _req('POST', \`/pipelines/\${pipeline}/claim\`, { stage, ...opts }),
15
15
  release: (pipeline, seq, opts = {}) => _req('POST', \`/pipelines/\${pipeline}/release/\${seq}\`, opts),
16
- batch_read: (pipeline, stage, opts = {}) => _req('GET', \`/pipelines/\${pipeline}/items?\${new URLSearchParams({ stage, ...opts })}\`),
16
+ query: (pipeline, stage, { filter, select, ...opts } = {}) => { const p = { stage, ...opts }; if (filter != null) p.filter = JSON.stringify(filter); if (select != null) p.select = Array.isArray(select) ? select.join(',') : select; return _req('GET', \`/pipelines/\${pipeline}/items?\${new URLSearchParams(p)}\`); },
17
17
  status: (pipeline) => _req('GET', \`/pipelines/\${pipeline}/status\`),
18
18
  };
19
19
  })();`;
@@ -19,8 +19,11 @@ def qq_claim(pipeline, stage, **opts):
19
19
  return _bridge_req('POST', f'/pipelines/{pipeline}/claim', {'stage': stage, **opts})
20
20
  def qq_release(pipeline, seq, **opts):
21
21
  return _bridge_req('POST', f'/pipelines/{pipeline}/release/{seq}', opts)
22
- def qq_batch_read(pipeline, stage, **opts):
23
- qs = urllib.parse.urlencode({'stage': stage, **opts})
22
+ def qq_query(pipeline, stage, filter=None, select=None, **opts):
23
+ params = {'stage': stage, **opts}
24
+ if filter is not None: params['filter'] = _json.dumps(filter)
25
+ if select is not None: params['select'] = ','.join(select) if isinstance(select, list) else select
26
+ qs = urllib.parse.urlencode(params)
24
27
  return _bridge_req('GET', f'/pipelines/{pipeline}/items?{qs}')
25
28
  def qq_status(pipeline):
26
29
  return _bridge_req('GET', f'/pipelines/{pipeline}/status')`;