@quantod/qq 1.1.19 → 1.3.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.
- package/dist/commands.d.ts +10 -7
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +204 -60
- package/dist/commands.js.map +1 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +56 -4
- package/dist/db.js.map +1 -1
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +43 -25
- package/dist/http.js.map +1 -1
- package/dist/index.js +25 -12
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +39 -40
- package/dist/mcp.js.map +1 -1
- package/dist/resources/chrome.md +3 -3
- package/dist/resources/guide.md +119 -116
- package/dist/sdk-templates/javascript.js +1 -1
- package/dist/sdk-templates/python.d.ts.map +1 -1
- package/dist/sdk-templates/python.js +5 -2
- package/dist/sdk-templates/python.js.map +1 -1
- package/package.json +1 -1
- package/src/commands.ts +196 -64
- package/src/db.ts +59 -4
- package/src/http.ts +46 -32
- package/src/index.ts +23 -14
- package/src/mcp.ts +40 -41
- package/src/resources/chrome.md +3 -3
- package/src/resources/guide.md +119 -116
- package/src/sdk-templates/javascript.ts +1 -1
- package/src/sdk-templates/python.ts +5 -2
package/src/resources/guide.md
CHANGED
|
@@ -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;
|
|
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,
|
|
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 `
|
|
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:
|
|
54
|
-
push Q/pending "url-2" payload:
|
|
55
|
-
push Q/pending "url-3" payload:
|
|
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` (
|
|
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
|
|
|
@@ -124,7 +121,7 @@ Parameters:
|
|
|
124
121
|
- `pipeline` (string)
|
|
125
122
|
- `stage` (string) — supports glob wildcards: `enriched/*`, `*`. Omit to claim from any stage.
|
|
126
123
|
- `id` (string, optional) — claim a specific item by id. Fails if not found or already claimed.
|
|
127
|
-
- `
|
|
124
|
+
- `sort_order` (`priority` | `fifo` | `lifo` | `random`, optional, default `priority`) — order in which unclaimed items are selected. `priority`: highest priority first, then lowest seq (default). `fifo`: lowest seq first, ignores priority. `lifo`: highest seq first, ignores priority. `random`: random selection, ignores priority. Ignored when claiming by `id`.
|
|
128
125
|
|
|
129
126
|
Returns:
|
|
130
127
|
```yaml
|
|
@@ -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` (
|
|
160
|
-
- `
|
|
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
|
|
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
|
-
|
|
193
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
- `
|
|
226
|
-
- `
|
|
207
|
+
- `payload_fields` (`"*"` | 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. **Note: this controls payload projection, not item filtering.**
|
|
208
|
+
- `payload_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
|
+
- `created_after`, `created_before`, `modified_after` (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
|
+
- `sort_order` (`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
|
|
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
|
---
|
|
@@ -257,7 +246,7 @@ Releases all stuck claimed items back to their stage. Use when agents have crash
|
|
|
257
246
|
Parameters:
|
|
258
247
|
- `pipeline` (string)
|
|
259
248
|
- `stage` (string, optional) — limit to a specific stage. Defaults to `*`
|
|
260
|
-
- `ids`, `
|
|
249
|
+
- `ids`, `created_after`, `created_before`, `modified_after` — optional filters
|
|
261
250
|
|
|
262
251
|
Returns:
|
|
263
252
|
```yaml
|
|
@@ -296,41 +285,42 @@ 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.
|
|
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)
|
|
304
|
-
- `claim` opts: `id` (string — claim a specific item), `
|
|
305
|
-
- `release` opts: `target` (string), `payload` (
|
|
306
|
-
- `
|
|
292
|
+
- `push` opts: `priority` (number)
|
|
293
|
+
- `claim` opts: `id` (string — claim a specific item), `sort_order` (`"priority"` | `"fifo"` | `"lifo"` | `"random"`)
|
|
294
|
+
- `release` opts: `target` (string), `payload` (object), `replace` (boolean), `priority` (number)
|
|
295
|
+
- `query` opts: `payload_fields` (`"*"` or string[] of dot-notation paths), `payload_filter` (object — MongoDB-style payload filter), `claimed` (boolean), `ids` (string, comma-separated), `created_after`, `created_before`, `modified_after` (epoch ms, ISO 8601, or relative e.g. `"-2h"`), `limit`, `offset` (number), `sort_order` (`"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 `
|
|
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?:
|
|
302
|
+
created: number, last_modified: number, payload?: object | null }
|
|
314
303
|
```
|
|
315
304
|
|
|
316
|
-
`claim` always includes `payload`. `
|
|
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
|
-
qq_push(pipeline, stage, id=None, payload=None, **opts)
|
|
321
|
-
qq_claim(pipeline, stage, **opts)
|
|
322
|
-
qq_release(pipeline, seq, **opts)
|
|
323
|
-
|
|
324
|
-
qq_status(pipeline)
|
|
310
|
+
qq_push(pipeline, stage, id=None, payload=None, **opts) → { 'id': str } | { 'error': str }
|
|
311
|
+
qq_claim(pipeline, stage, **opts) → dict | { 'error': str }
|
|
312
|
+
qq_release(pipeline, seq, **opts) → { 'ok': True } | { 'error': str }
|
|
313
|
+
qq_query(pipeline, stage, payload_filter=None, payload_fields=None, **opts) → list[dict] | { 'error': str }
|
|
314
|
+
qq_status(pipeline) → { 'description': str|None, 'stats': dict } | { 'error': str }
|
|
325
315
|
```
|
|
326
316
|
|
|
327
317
|
All Python functions return `{ 'error': str }` on failure — never raise (except `BridgeError` for connection failures). Error messages are identical to those in the MCP tool reference above.
|
|
328
318
|
|
|
329
319
|
---
|
|
330
320
|
|
|
331
|
-
###
|
|
321
|
+
### upload_file
|
|
332
322
|
|
|
333
|
-
|
|
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
|
|
|
@@ -340,8 +330,8 @@ Parameters:
|
|
|
340
330
|
- `path` (string) — path to the file. Supports `$VAR` expansion.
|
|
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
|
-
- `
|
|
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`:
|
|
333
|
+
- `delete_after` (boolean, optional) — delete the file after loading. Default: true.
|
|
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:
|
|
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` —
|
|
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:
|
|
@@ -384,7 +378,7 @@ Reads and returns the raw content of a file on the host filesystem. Use this to
|
|
|
384
378
|
|
|
385
379
|
Parameters:
|
|
386
380
|
- `path` (string) — path to the file. Supports `$VAR` expansion.
|
|
387
|
-
- `
|
|
381
|
+
- `delete_after` (boolean, optional) — delete the file after reading. Default: true.
|
|
388
382
|
|
|
389
383
|
Returns the file content as plain text.
|
|
390
384
|
|
|
@@ -393,24 +387,22 @@ Errors:
|
|
|
393
387
|
|
|
394
388
|
---
|
|
395
389
|
|
|
396
|
-
###
|
|
390
|
+
### upload_data
|
|
397
391
|
|
|
398
|
-
Same as `
|
|
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 `
|
|
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 `
|
|
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
|
|
427
|
+
// Bulk push — payload is a plain object
|
|
436
428
|
for (const url of urls) {
|
|
437
|
-
await QQ.push(pipeline, 'pending', 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.
|
|
441
|
+
const done = await QQ.query(pipeline, 'done', { payload_fields: '*' });
|
|
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
|
+
payload_filter: { score: { $gt: 80 }, status: 'active' },
|
|
449
|
+
payload_fields: ['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:
|
|
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:
|
|
469
|
+
await QQ.release(pipeline, item.seq, { target: 'failed', payload: { error: e.message } });
|
|
472
470
|
}
|
|
473
471
|
}
|
|
474
472
|
```
|
|
475
473
|
|
|
476
|
-
###
|
|
474
|
+
### Working with structured payloads
|
|
477
475
|
|
|
478
|
-
|
|
476
|
+
Payloads are plain objects. `release` merges them by default (json_patch); `replace: true` overwrites:
|
|
479
477
|
|
|
480
478
|
```javascript
|
|
481
|
-
// Push with
|
|
482
|
-
await QQ.push(pipeline, 'pending', 'item-1',
|
|
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
|
-
//
|
|
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
|
|
489
|
-
|
|
490
|
-
await QQ.release(pipeline, item.seq, {
|
|
491
|
-
|
|
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,
|
|
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 =
|
|
514
|
+
items = qq_query(pipeline, "done", payload_fields="*")
|
|
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
|
+
payload_filter={"score": {"$gt": 80}, "status": "active"},
|
|
521
|
+
payload_fields=["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
|
-
##
|
|
539
|
+
## Structured Payload and Merge Semantics
|
|
539
540
|
|
|
540
|
-
|
|
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
|
-
|
|
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 `
|
|
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
|
|
571
|
+
**Filters in query / unstick**: `claimed`, `ids`, `created_after`, `created_before`, `modified_after` (epoch ms, ISO 8601, or relative: `"-2h"`, `"-1d"`, `"-30m"`, `"-1w"`), `limit`, `offset`. **query** also accepts `sort_order` (`priority` | `fifo` | `lifo` | `random`, default `fifo`), `payload_filter` (MongoDB-style payload filter), and `payload_fields` (`"*"` 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
|
-
|
|
16
|
+
query: (pipeline, stage, { payload_filter, payload_fields, ...opts } = {}) => { const p = { stage, ...opts }; if (payload_filter != null) p.payload_filter = JSON.stringify(payload_filter); if (payload_fields != null) p.payload_fields = Array.isArray(payload_fields) ? payload_fields.join(',') : payload_fields; 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
|
|
23
|
-
|
|
22
|
+
def qq_query(pipeline, stage, payload_filter=None, payload_fields=None, **opts):
|
|
23
|
+
params = {'stage': stage, **opts}
|
|
24
|
+
if payload_filter is not None: params['payload_filter'] = _json.dumps(payload_filter)
|
|
25
|
+
if payload_fields is not None: params['payload_fields'] = ','.join(payload_fields) if isinstance(payload_fields, list) else payload_fields
|
|
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')`;
|