@nanobpm/nano-workforce 0.151.0 → 0.151.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/app/deliveryGraphRun.test.ts +49 -1
- package/app/service.ts +12 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [0.151.1](https://github.com/nanobpm/nano-workforce/compare/v0.151.0...v0.151.1) (2026-08-28)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
* **delivery-graph:** read parked human nodes via searchUserTasks until engine projects USER_TASK wait states ([#595](https://github.com/nanobpm/nano-workforce/issues/595)) ([5dfb3fe](https://github.com/nanobpm/nano-workforce/commit/5dfb3fed1b0483c3a110979bd0aa7802ef3b17d2)), closes [#542](https://github.com/nanobpm/nano-workforce/issues/542) [Magikcraft/nano-bpm#1042](https://github.com/Magikcraft/nano-bpm/issues/1042) [nanobpm/nano-workforce#594](https://github.com/nanobpm/nano-workforce/issues/594)
|
|
6
|
+
|
|
1
7
|
## [0.151.0](https://github.com/nanobpm/nano-workforce/compare/v0.150.4...v0.151.0) (2026-08-28)
|
|
2
8
|
|
|
3
9
|
### Features
|
|
@@ -115,7 +115,55 @@ test("pollDeliveryGraphPhase: a numeric engine processInstanceKey still matches
|
|
|
115
115
|
// The engine can yield a NUMERIC key; the poller compares against the string process_key.
|
|
116
116
|
const engine = {
|
|
117
117
|
searchProcessInstances: async () => [{ processInstanceKey: 12345, state: "COMPLETED" }],
|
|
118
|
-
|
|
118
|
+
searchUserTasks: async () => [],
|
|
119
|
+
};
|
|
120
|
+
await pollDeliveryGraphPhase(data, engine as never);
|
|
121
|
+
assertEquals((await runs.get("rk"))?.status, "done");
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// ── pollDeliveryGraphPhase: engine 422 on USER_TASK wait-state read (nano-bpm#1042) ────────────
|
|
126
|
+
// The deployed engine's wait-state read model only accepts JOB | MESSAGE, so a `waitStateType:
|
|
127
|
+
// "USER_TASK"` filter 422s on a live gateway. This pass reads parks via `searchUserTasks({ state:
|
|
128
|
+
// "CREATED" })` — NOT `searchElementInstanceWaitStates` — precisely so that 422 can never reject the
|
|
129
|
+
// `Promise.all` and skip reconciliation. Guard both projections this pass owns against regressing back
|
|
130
|
+
// onto the wait-state channel: the parked-label projection AND the COMPLETED → `done` transition.
|
|
131
|
+
test("pollDeliveryGraphPhase: with an engine that 422s on waitStateType=USER_TASK, the parked label still projects (reads via searchUserTasks)", async () => {
|
|
132
|
+
await withData(async (data) => {
|
|
133
|
+
const runs = deliveryGraphRuns(data);
|
|
134
|
+
const humanEl = humanTaskElementId("publish");
|
|
135
|
+
await runs.insert({
|
|
136
|
+
...claimRow("running"),
|
|
137
|
+
process_key: "PI-1",
|
|
138
|
+
human_labels: JSON.stringify({ [humanEl]: "run the manual OTP publish" }),
|
|
139
|
+
});
|
|
140
|
+
// A client that rejects the wait-state read the way a live gateway does (HTTP 422). If the poller
|
|
141
|
+
// reached for it, the throw would reject the Promise.all and reconciliation would be skipped.
|
|
142
|
+
const engine = {
|
|
143
|
+
searchProcessInstances: async () => [{ processInstanceKey: "PI-1", state: "ACTIVE" }],
|
|
144
|
+
searchUserTasks: async () => [{ userTaskKey: "ut-1", elementId: humanEl }],
|
|
145
|
+
searchElementInstanceWaitStates: async () => {
|
|
146
|
+
throw new Error("HttpSdkError: HTTP 422 — filter.waitStateType did not match any variant of untagged enum WaitStateTypeFilterProperty");
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
await pollDeliveryGraphPhase(data, engine as never);
|
|
150
|
+
const row = await runs.get("rk");
|
|
151
|
+
assertEquals(row?.status, "running");
|
|
152
|
+
assertEquals(row?.phase, "Parked on human node: run the manual OTP publish");
|
|
153
|
+
assertEquals(row?.phase_node_id, humanEl);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("pollDeliveryGraphPhase: with an engine that 422s on waitStateType=USER_TASK, a COMPLETED instance still reconciles to done", async () => {
|
|
158
|
+
await withData(async (data) => {
|
|
159
|
+
const runs = deliveryGraphRuns(data);
|
|
160
|
+
await runs.insert({ ...claimRow("running"), process_key: "PI-2" });
|
|
161
|
+
const engine = {
|
|
162
|
+
searchProcessInstances: async () => [{ processInstanceKey: "PI-2", state: "COMPLETED" }],
|
|
163
|
+
searchUserTasks: async () => [],
|
|
164
|
+
searchElementInstanceWaitStates: async () => {
|
|
165
|
+
throw new Error("HttpSdkError: HTTP 422 — filter.waitStateType did not match any variant of untagged enum WaitStateTypeFilterProperty");
|
|
166
|
+
},
|
|
119
167
|
};
|
|
120
168
|
await pollDeliveryGraphPhase(data, engine as never);
|
|
121
169
|
assertEquals((await runs.get("rk"))?.status, "done");
|
package/app/service.ts
CHANGED
|
@@ -2391,13 +2391,19 @@ export async function pollEpicPhase(
|
|
|
2391
2391
|
* a graph that ends normally would otherwise stay `running` forever). Generalises the `epic_phase`
|
|
2392
2392
|
* derived-phase machinery to a graph whose element ids aren't known ahead of time: the parked-node
|
|
2393
2393
|
* label is derived from the run row's stamped `human_labels` + the instance's live USER_TASK parks.
|
|
2394
|
-
* The parked node is
|
|
2395
|
-
*
|
|
2396
|
-
*
|
|
2397
|
-
* `
|
|
2394
|
+
* The parked node is read via `searchUserTasks({ state: "CREATED" })` rather than the unified
|
|
2395
|
+
* element-instance wait-state channel (`searchElementInstanceWaitStates({ waitStateType: "USER_TASK" })`,
|
|
2396
|
+
* S8, #542/nano-ide#473): the deployed engine's wait-state read model only accepts `JOB | MESSAGE`, so
|
|
2397
|
+
* a `USER_TASK` filter 422s on a live gateway, rejecting the whole `Promise.all` and skipping this pass's
|
|
2398
|
+
* reconciliation entirely (the parked label never updates and the COMPLETED → `done` transition this pass
|
|
2399
|
+
* OWNS never fires, leaving a finished graph `running` forever). `deriveDeliveryPhase` consumes only
|
|
2400
|
+
* `elementId` from the park rows, which `searchUserTasks` supplies the same way.
|
|
2401
|
+
* TODO(Magikcraft/nano-bpm#1042): re-fold onto `searchElementInstanceWaitStates({ waitStateType:
|
|
2402
|
+
* "USER_TASK" })` once the engine projects `USER_TASK` wait states and that engine is deployed.
|
|
2403
|
+
* Scoped to `running` rows (an `awaiting-approval` run has no instance yet), so it stays O(in-flight). */
|
|
2398
2404
|
export async function pollDeliveryGraphPhase(
|
|
2399
2405
|
data: DataLayer,
|
|
2400
|
-
engine: Pick<EngineClient, "searchProcessInstances" | "
|
|
2406
|
+
engine: Pick<EngineClient, "searchProcessInstances" | "searchUserTasks">,
|
|
2401
2407
|
) {
|
|
2402
2408
|
for (const run of await deliveryGraphRuns(data).find({ status: "running" })) {
|
|
2403
2409
|
if (!run.process_key) continue;
|
|
@@ -2405,7 +2411,7 @@ export async function pollDeliveryGraphPhase(
|
|
|
2405
2411
|
try {
|
|
2406
2412
|
const [snapshots, parks] = await Promise.all([
|
|
2407
2413
|
engine.searchProcessInstances({ processInstanceKeys: [processKey] }),
|
|
2408
|
-
engine.
|
|
2414
|
+
engine.searchUserTasks({ processInstanceKey: processKey, state: "CREATED" }),
|
|
2409
2415
|
]);
|
|
2410
2416
|
const state = snapshots.find((s) => String(s.processInstanceKey) === processKey)?.state ?? null;
|
|
2411
2417
|
const projection = deriveDeliveryPhase(state, parks, parseHumanLabels(run.human_labels));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanobpm/nano-workforce",
|
|
3
|
-
"version": "0.151.
|
|
3
|
+
"version": "0.151.1",
|
|
4
4
|
"description": "Nano Workforce — an Agent Graph Orchestration application for Agentic SDLC: durable BPMN processes that coordinate a graph of AI agents across the software delivery lifecycle.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "main.ts",
|