@hotmeshio/long-tail 0.21.1 → 0.21.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.
|
@@ -46,8 +46,11 @@ export declare function accumulateItemBySignalKey(input: {
|
|
|
46
46
|
} & Omit<AccumulateItemInput, 'assertClaim'>, auth: LTApiAuth): Promise<LTApiResult>;
|
|
47
47
|
/**
|
|
48
48
|
* Add selecting the container by metadata facet, the faceted sibling of
|
|
49
|
-
* {@link accumulateItem}.
|
|
50
|
-
*
|
|
49
|
+
* {@link accumulateItem}. The facet names a container, so only pending rows
|
|
50
|
+
* carrying the accumulator declaration are candidates: a release or
|
|
51
|
+
* remediation row that shares the facet is never picked, and when no open
|
|
52
|
+
* container shares it the answer is 404. RBAC folds into the SDK's atomic
|
|
53
|
+
* facet selection as a flat role filter. Schema enforcement is two-phase when enforcing
|
|
51
54
|
* roles exist and a payload is present: the row is picked first, validated
|
|
52
55
|
* against ITS schema, then written by asserted id; a row that went terminal
|
|
53
56
|
* between phases surfaces as a 409.
|
|
@@ -65,6 +65,25 @@ function isAccumulator(escalation) {
|
|
|
65
65
|
function isBatch(escalation) {
|
|
66
66
|
return Array.isArray(escalation.metadata?.[escalation_1.ESCALATION_BATCH_KEYS.PENDING]);
|
|
67
67
|
}
|
|
68
|
+
const NO_ACCUMULATOR_FOR_FACET = 'No pending accumulator found for this metadata';
|
|
69
|
+
/**
|
|
70
|
+
* The container a facet names: the highest priority pending row that carries
|
|
71
|
+
* the facet AND the accumulator declaration, within the given roles (null =
|
|
72
|
+
* unscoped). A release or remediation row sharing the facet is never a
|
|
73
|
+
* candidate, so a by-facet add lands on the open container or nowhere.
|
|
74
|
+
*/
|
|
75
|
+
async function pickAccumulatorByFacet(key, value, roles) {
|
|
76
|
+
if (roles !== null && roles.length === 0)
|
|
77
|
+
return null;
|
|
78
|
+
const found = await escalationService.searchByFacets({
|
|
79
|
+
roles: roles ?? undefined,
|
|
80
|
+
facets: { [key]: value },
|
|
81
|
+
status: 'pending',
|
|
82
|
+
exists: [escalation_1.ESCALATION_ACCUMULATE_KEYS.COUNT],
|
|
83
|
+
limit: 1,
|
|
84
|
+
});
|
|
85
|
+
return found.escalations[0] ?? null;
|
|
86
|
+
}
|
|
68
87
|
function reciprocalSelectorCount(r) {
|
|
69
88
|
return (r.id ? 1 : 0) + (r.signalKey ? 1 : 0) + (r.key !== undefined ? 1 : 0);
|
|
70
89
|
}
|
|
@@ -89,8 +108,7 @@ async function gateReciprocal(input, auth) {
|
|
|
89
108
|
}
|
|
90
109
|
else {
|
|
91
110
|
const writeScope = await (0, helpers_1.getEscalationWriteScope)(auth.userId);
|
|
92
|
-
|
|
93
|
-
row = found.escalations[0] ?? null;
|
|
111
|
+
row = await pickAccumulatorByFacet(input.key, String(input.value), writeScope.global ? null : writeScope.allRoles);
|
|
94
112
|
}
|
|
95
113
|
if (!row)
|
|
96
114
|
return { error: { status: 404, error: 'Reciprocal escalation not found' } };
|
|
@@ -220,8 +238,11 @@ async function accumulateItemBySignalKey(input, auth) {
|
|
|
220
238
|
}
|
|
221
239
|
/**
|
|
222
240
|
* Add selecting the container by metadata facet, the faceted sibling of
|
|
223
|
-
* {@link accumulateItem}.
|
|
224
|
-
*
|
|
241
|
+
* {@link accumulateItem}. The facet names a container, so only pending rows
|
|
242
|
+
* carrying the accumulator declaration are candidates: a release or
|
|
243
|
+
* remediation row that shares the facet is never picked, and when no open
|
|
244
|
+
* container shares it the answer is 404. RBAC folds into the SDK's atomic
|
|
245
|
+
* facet selection as a flat role filter. Schema enforcement is two-phase when enforcing
|
|
225
246
|
* roles exist and a payload is present: the row is picked first, validated
|
|
226
247
|
* against ITS schema, then written by asserted id; a row that went terminal
|
|
227
248
|
* between phases surfaces as a 409.
|
|
@@ -251,12 +272,13 @@ async function accumulateItemByMetadata(input, auth) {
|
|
|
251
272
|
roles: allowedRoles ?? undefined,
|
|
252
273
|
reciprocal: reciprocal.reciprocal,
|
|
253
274
|
});
|
|
275
|
+
if (result.outcome === 'not-found')
|
|
276
|
+
return { status: 404, error: NO_ACCUMULATOR_FOR_FACET };
|
|
254
277
|
return accumulateOutcomeResult(result, itemKey, result.escalation?.workflow_id ?? null);
|
|
255
278
|
}
|
|
256
|
-
const
|
|
257
|
-
const row = found.escalations[0];
|
|
279
|
+
const row = await pickAccumulatorByFacet(key, value, allowedRoles);
|
|
258
280
|
if (!row)
|
|
259
|
-
return { status: 404, error:
|
|
281
|
+
return { status: 404, error: NO_ACCUMULATOR_FOR_FACET };
|
|
260
282
|
if (!isAccumulator(row))
|
|
261
283
|
return { status: 400, error: 'Escalation is not an accumulator' };
|
|
262
284
|
const payload = await gatePayload(row, input.payload, auth.userId);
|
|
@@ -358,6 +380,8 @@ async function removeItemByMetadata(input, auth) {
|
|
|
358
380
|
const result = await escalationService.removeAccumulatedItemByMetadata(key, value, {
|
|
359
381
|
itemKey, actor: auth.userId, roles: allowedRoles ?? undefined, reciprocal: reciprocal.reciprocal,
|
|
360
382
|
});
|
|
383
|
+
if (result.outcome === 'not-found')
|
|
384
|
+
return { status: 404, error: NO_ACCUMULATOR_FOR_FACET };
|
|
361
385
|
return removeOutcomeResult(result, itemKey);
|
|
362
386
|
}
|
|
363
387
|
catch (err) {
|
package/docs/accumulator.md
CHANGED
|
@@ -67,6 +67,8 @@ A batch wait may add `partialOnTimeout: true`; it stores `envelope.batch_partial
|
|
|
67
67
|
| Read the collection | `GET /api/escalations/:id/items`, `GET /api/escalations/items-by-signal-key` | `getItems`, `getItemsBySignalKey` | `get_escalation_items` (admin; `id` or `signalKey`) |
|
|
68
68
|
| Scan | the `accumulate` verb; item mode answers `no_open_container` when the container closed; templates read `{claim.<facet>}` and `{item.<facet>}` | | |
|
|
69
69
|
|
|
70
|
+
A facet selector (`accumulateByMetadata`, `removeItemByMetadata`, a reciprocal named by `key` + `value`, the scan verb's item mode) considers only pending rows that carry the accumulator declaration, ordered by priority then age. A release or remediation row sharing the container's facet is never picked; with no open container the answer is `not-found`, and the scan answers `no_open_container`.
|
|
71
|
+
|
|
70
72
|
An add takes `itemKey` (1 to 128 characters), an optional `payload` (validated against the container role's versioned form when present), an optional `metadata` patch (reserved keys rejected), `assertClaim` (by id), and an optional `reciprocal`.
|
|
71
73
|
|
|
72
74
|
### Outcomes
|
|
@@ -78,7 +80,7 @@ An add takes `itemKey` (1 to 128 characters), an optional `payload` (validated a
|
|
|
78
80
|
| `duplicate-item` | 409 | the key is already held (`unique`), row untouched |
|
|
79
81
|
| `full` | 409 | `max` held and the key is new (`resolveAtMax: false`, or a race with the completing add) |
|
|
80
82
|
| `not-accumulator` | 400 | the row carries no `accumulate` declaration |
|
|
81
|
-
| `not-found` | 404 | unknown row,
|
|
83
|
+
| `not-found` | 404 | unknown row, outside the caller's scope on the ingress forms, or (by facet) no pending accumulator shares the facet; rows without the declaration are never candidates |
|
|
82
84
|
| `already-resolved`, `already-expired`, `already-cancelled` | 409 | the row is terminal |
|
|
83
85
|
| `claimed-by-other`, `claim-expired` | 409 | the `assertClaim` assertion failed |
|
|
84
86
|
| `reciprocal-not-found` | 404 | the reciprocal selector matched nothing the caller may act on |
|
|
@@ -110,4 +112,4 @@ Every accepted add and removal publishes `escalation.updated` for the container
|
|
|
110
112
|
|
|
111
113
|
## Versions
|
|
112
114
|
|
|
113
|
-
HotMesh 0.29.0 (`accumulate`, `partialOnTimeout`, timeout delivery, reciprocal statement) and long-tail 0.21.1 (root export of `conditionalAccumulator`, `no_open_container`, claim and item template tokens, items by signal key).
|
|
115
|
+
HotMesh 0.29.0 (`accumulate`, `partialOnTimeout`, timeout delivery, reciprocal statement) and long-tail 0.21.1 (root export of `conditionalAccumulator`, `no_open_container`, claim and item template tokens, items by signal key); HotMesh 0.29.1 and long-tail 0.21.2 (facet selectors pick pending accumulators only).
|
|
@@ -1166,7 +1166,7 @@ Add ONE item to an open accumulator escalation (a `conditionalAccumulator` wait
|
|
|
1166
1166
|
| `assertClaim` | `boolean` | By id only. Require the caller's own live claim, asserted atomically |
|
|
1167
1167
|
| `reciprocal` | `object` | A second accumulator row written in the same statement, both or neither. Exactly one of `id`, `signalKey`, or `key` + `value`; optional `payload` stored on its entry |
|
|
1168
1168
|
|
|
1169
|
-
**By signal key:** `{ signalKey, itemKey, payload?, metadata?, reciprocal? }`. **By metadata:** `{ key, value, itemKey, payload?, metadata?, restrictRoles?, reciprocal? }` selects the highest priority pending accumulator whose metadata contains the key/value, within the caller's write scope intersected with `restrictRoles`.
|
|
1169
|
+
**By signal key:** `{ signalKey, itemKey, payload?, metadata?, reciprocal? }`. **By metadata:** `{ key, value, itemKey, payload?, metadata?, restrictRoles?, reciprocal? }` selects the highest priority pending accumulator whose metadata contains the key/value, within the caller's write scope intersected with `restrictRoles`. Only rows carrying the accumulator declaration are candidates: a release or remediation row that shares the facet is never picked, and when no open container shares it the answer is 404 `No pending accumulator found for this metadata`. The same rule selects a reciprocal named by `key` + `value`.
|
|
1170
1170
|
|
|
1171
1171
|
**Response 200 (interim):**
|
|
1172
1172
|
|
|
@@ -887,7 +887,7 @@ await lt.escalations.accumulateByMetadata({ key: 'binKey', value: binKey, itemKe
|
|
|
887
887
|
| `metadata` | `object` | No | Merge patch for the container metadata, same statement; reserved `accumulate_*` keys rejected |
|
|
888
888
|
| `assertClaim` | `boolean` | No | By id only: require the caller's own live claim |
|
|
889
889
|
| `reciprocal` | `object` | No | `{ id \| signalKey \| key + value, payload? }`: a second accumulator row written in the same statement, both or neither |
|
|
890
|
-
| `restrictRoles` | `string[]` | No | By metadata only: expected queues, intersected with the caller's write scope |
|
|
890
|
+
| `restrictRoles` | `string[]` | No | By metadata only: expected queues, intersected with the caller's write scope. The facet selects among pending accumulator rows only; other rows sharing it are never candidates, and none open answers 404 |
|
|
891
891
|
|
|
892
892
|
**Returns:** `LTApiResult<{ outcome: 'accepted' \| 'completed', count, remaining, escalationId, signaled?, workflowId?, reciprocal? }>`. `remaining` is `null` on an unbounded accumulator. 409 on `Item already held`, `Accumulator is full`, claim blocks, terminal rows, and reciprocal blocks; 400 on a non-accumulator; 404 non-disclosure.
|
|
893
893
|
|
package/docs/scan-codes.md
CHANGED
|
@@ -118,7 +118,9 @@ through the scheme facet, reads that facet from the row (a bag row carrying
|
|
|
118
118
|
`binKey: "B-7"`), and adds the target to the pending accumulator whose
|
|
119
119
|
metadata carries the same value, writing the item's row as the reciprocal in
|
|
120
120
|
the same statement (`reciprocal: false` skips it; `containerRoles` names the
|
|
121
|
-
container queues).
|
|
121
|
+
container queues). The container pick considers only pending rows that carry
|
|
122
|
+
the accumulator declaration, so a release row sharing the facet during a
|
|
123
|
+
pack-out is never the target. Without it the scanned target is the CONTAINER and the step
|
|
122
124
|
adds `params.itemKey` (a template) to it. An item with no row or no container
|
|
123
125
|
facet falls through to the next step; a container already holding the item
|
|
124
126
|
reports a conflict. When the item's row exists but no pending container
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hotmeshio/long-tail",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.2",
|
|
4
4
|
"description": "Long Tail Workflows — Durable AI workflows with human-in-the-loop escalation. Powered by PostgreSQL.",
|
|
5
5
|
"main": "./build/index.js",
|
|
6
6
|
"types": "./build/index.d.ts",
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"@anthropic-ai/sdk": "^0.92.0",
|
|
76
76
|
"@aws-sdk/client-s3": "^3.1017.0",
|
|
77
77
|
"@aws-sdk/s3-request-presigner": "^3.1045.0",
|
|
78
|
-
"@hotmeshio/hotmesh": "^0.29.
|
|
78
|
+
"@hotmeshio/hotmesh": "^0.29.1",
|
|
79
79
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
80
80
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.215.0",
|
|
81
81
|
"@opentelemetry/resources": "^2.5.1",
|