@nanobpm/nano-workforce 0.40.0 → 0.40.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## [0.40.2](https://github.com/nanobpm/nano-workforce/compare/v0.40.1...v0.40.2) (2026-08-11)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **merge-loop:** escape wait-landed when a queued PR is evicted on conflict ([#117](https://github.com/nanobpm/nano-workforce/issues/117)) ([df7f850](https://github.com/nanobpm/nano-workforce/commit/df7f85050624f11490d715c90e054a5c7fb6d0fa)), closes [Magikcraft/nano-bpm#727](https://github.com/Magikcraft/nano-bpm/issues/727)
7
+
8
+ ## [0.40.1](https://github.com/nanobpm/nano-workforce/compare/v0.40.0...v0.40.1) (2026-08-11)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **convergence:** safe default for resultless review rounds ([#116](https://github.com/nanobpm/nano-workforce/issues/116)) ([de85188](https://github.com/nanobpm/nano-workforce/commit/de85188f57b69496596f4c41cf555c9f6f2090da))
14
+
1
15
  # [0.40.0](https://github.com/nanobpm/nano-workforce/compare/v0.39.3...v0.40.0) (2026-08-11)
2
16
 
3
17
 
@@ -95,11 +95,17 @@ test("a padded question is persisted trimmed (no whitespace drift)", async () =>
95
95
  assertEquals((updates.pull_requests![0] as any).patch.open_escalation_question, "needs a decision", "denormalised question is trimmed too");
96
96
  });
97
97
 
98
- // A round that fell through the `gw-status` default (no `converged`/`addressed` status and no
99
- // question the prompt-less-agent failure behind the empty "(no question provided)" escalations
100
- // on Magikcraft/nano-bpm #597/#599) must NOT throw (which parked an un-remediable JobNoRetries
101
- // incident). It now opens an *answerable* escalation with a fabricated, concrete question and the
102
- // agent's transcript attached, so a human can unblock the loop entirely from the UI.
98
+ // Defence-in-depth for the persist-escalation worker: if the gateway ever DOES route a
99
+ // blank-status / blank-question job here (an agent-raised `needs_input`/`blocked` with no
100
+ // question, or the max-rounds / review-stalled arms), the worker must NOT throw throwing
101
+ // parked an un-remediable JobNoRetries incident (the empty "(no question provided)" escalations
102
+ // on Magikcraft/nano-bpm #597/#599). It opens an *answerable* escalation with a fabricated,
103
+ // concrete question and the agent's transcript attached, so a human can unblock the loop from
104
+ // the UI.
105
+ //
106
+ // NOTE: the `gw-status` gateway no longer routes an empty/unknown status here — that now
107
+ // defaults to `f_addressed` and re-enters the review wait (see roundResultDefault.test.ts).
108
+ // This fabrication path stays as a worker-level backstop for the explicit escalation arms.
103
109
  test("blank question fabricates an answerable escalation (no throw, no incident)", async () => {
104
110
  for (const question of [undefined, "", " "]) {
105
111
  const { app, inserts, updates } = fakeApp();
@@ -0,0 +1,40 @@
1
+ // The merge-loop enqueues a "ready" PR (mergeStatus="queued") and parks at `wait-landed`. This
2
+ // decision drives what the poller does next from the PR's live GitHub state. The regression it
3
+ // guards: a PR that develops a merge CONFLICT after being enqueued (#727/instance 729) must be
4
+ // EVICTED back to the mergeable gate, not left waiting forever — while a PR still legitimately in
5
+ // the queue (reported BLOCKED/UNSTABLE by GitHub) must keep waiting, never be falsely evicted.
6
+ import { test } from "node:test";
7
+ import { assertEquals } from "#test-assert";
8
+ import type { PrState } from "./github.ts";
9
+ import { queuedVerdict } from "./service.ts";
10
+
11
+ function st(over: Partial<PrState>): PrState {
12
+ return {
13
+ merged: false,
14
+ mergeStateStatus: "CLEAN",
15
+ failingChecks: 0,
16
+ failingCheckNames: [],
17
+ presentCheckNames: [],
18
+ totalChecks: 0,
19
+ isDraft: false,
20
+ headRefOid: null,
21
+ ...over,
22
+ };
23
+ }
24
+
25
+ test("a landed PR advances (merge-landed)", () => {
26
+ assertEquals(queuedVerdict(st({ merged: true, mergeStateStatus: "CLEAN" })), "landed");
27
+ // `merged` wins even if a stale mergeStateStatus lags.
28
+ assertEquals(queuedVerdict(st({ merged: true, mergeStateStatus: "DIRTY" })), "landed");
29
+ });
30
+
31
+ test("a DIRTY (conflicting) PR is evicted — this is the #727 wedge", () => {
32
+ assertEquals(queuedVerdict(st({ merged: false, mergeStateStatus: "DIRTY" })), "evicted");
33
+ });
34
+
35
+ test("a PR still legitimately in the queue keeps waiting (never falsely evicted)", () => {
36
+ // Queuing PRs commonly report these; none is a conflict, so none may evict.
37
+ for (const s of ["CLEAN", "BLOCKED", "UNSTABLE", "BEHIND", "HAS_HOOKS", "UNKNOWN", "DRAFT"]) {
38
+ assertEquals(queuedVerdict(st({ merged: false, mergeStateStatus: s })), "waiting", s);
39
+ }
40
+ });
@@ -0,0 +1,94 @@
1
+ // Structural regression guard for the review-round "safe default" routing.
2
+ //
3
+ // A review round that exits without a machine-readable result (empty/unknown status)
4
+ // used to fall through the `gw-status` exclusive gateway's `default="f_escalate"`
5
+ // arm and escalate to a human — even after a benign rebase/force-push with no
6
+ // reviewer comments. That is premature: the round-cap gate (`gw-guard`) and the
7
+ // review-wait timeout already provide the human-escalation safety nets, and
8
+ // `persist-round` defaults an absent status to `addressed`.
9
+ //
10
+ // The fix inverts the gateway: escalation is now an EXPLICIT arm gated on
11
+ // `needs_input`/`blocked`, and `f_addressed` is the default, so any unknown/empty
12
+ // status re-enters the durable review wait instead of paging a human.
13
+ //
14
+ // This is a pure text assertion over the committed BPMN (no engine), matching the
15
+ // repo's lightweight model-guard style (see mergeRebaseArm.test.ts).
16
+
17
+ import { test } from "node:test";
18
+ import { assert, assertStringIncludes } from "#test-assert";
19
+ import { readFileSync } from "node:fs";
20
+
21
+ const bpmn = readFileSync("resources/processes/convergence-loop.bpmn", "utf8");
22
+
23
+ // Collapse whitespace so attribute-order / line-wrapping churn doesn't make the assertions brittle.
24
+ const flat = bpmn.replace(/\s+/g, " ");
25
+
26
+ // The `<sequenceFlow id="...">` element (whole element, up to its close), whether
27
+ // self-closing or with children. Returns the matched text or null.
28
+ //
29
+ // The body branch uses a tempered negative lookahead — `(?:(?!<bpmn:sequenceFlow\b).)*?`
30
+ // — so it can never over-match across a following `<bpmn:sequenceFlow>` element; it
31
+ // captures only up to *this* element's own close.
32
+ function flowElement(id: string): string | null {
33
+ const re = new RegExp(
34
+ `<bpmn:sequenceFlow\\b[^>]*?\\bid="${id}"[^>]*?(?:/>|>(?:(?!<bpmn:sequenceFlow\\b).)*?</bpmn:sequenceFlow>)`,
35
+ );
36
+ const m = flat.match(re);
37
+ return m ? m[0] : null;
38
+ }
39
+
40
+ // Every `<bpmn:sequenceFlow>` element (whole element, self-closing or with children),
41
+ // using the same tempered-lookahead boundary as flowElement() so a captured element can
42
+ // never bleed into the next one.
43
+ function allFlows(): string[] {
44
+ const re = new RegExp(
45
+ "<bpmn:sequenceFlow\\b[^>]*?(?:/>|>(?:(?!<bpmn:sequenceFlow\\b).)*?</bpmn:sequenceFlow>)",
46
+ "g",
47
+ );
48
+ return flat.match(re) ?? [];
49
+ }
50
+
51
+ test("gw-status defaults to the addressed arm, not escalation", () => {
52
+ const gw = flat.match(/<bpmn:exclusiveGateway\b[^>]*\bid="gw-status"[^>]*>/);
53
+ assert(gw, "gw-status gateway missing");
54
+ assertStringIncludes(gw[0], 'default="f_addressed"');
55
+ // The old premature-escalation default must be gone.
56
+ assert(
57
+ !/id="gw-status"[^>]*default="f_escalate"/.test(flat),
58
+ "gw-status must no longer default to escalation",
59
+ );
60
+ });
61
+
62
+ test("escalation is an explicit needs_input/blocked arm", () => {
63
+ const esc = flowElement("f_escalate");
64
+ assert(esc, "f_escalate flow missing");
65
+ assertStringIncludes(esc, 'targetRef="persist-escalation"');
66
+ // Escalation now only fires on an explicit human-blocking status.
67
+ assertStringIncludes(esc, 'status = "needs_input" or status = "blocked"');
68
+ });
69
+
70
+ test("the default (addressed) arm carries no condition and re-enters the guard", () => {
71
+ const addressed = flowElement("f_addressed");
72
+ assert(addressed, "f_addressed flow missing");
73
+ assertStringIncludes(addressed, 'targetRef="gw-guard"');
74
+ // A default flow must have NO conditionExpression.
75
+ assert(
76
+ !/conditionExpression/.test(addressed),
77
+ "f_addressed is the default flow and must not carry a conditionExpression",
78
+ );
79
+ });
80
+
81
+ test("regression: an empty/unknown status no longer routes to persist-escalation", () => {
82
+ // Escalation is reachable ONLY via an explicit condition (the needs_input/blocked arm),
83
+ // never as a catch-all default. Enumerate EVERY sequenceFlow into `persist-escalation`
84
+ // and assert each one carries a conditionExpression — so no future edit can slip an
85
+ // unconditional (default) arm into escalation.
86
+ const intoEscalation = allFlows().filter((f) => /targetRef="persist-escalation"/.test(f));
87
+ assert(intoEscalation.length > 0, "no flow targets persist-escalation");
88
+ for (const f of intoEscalation) {
89
+ assertStringIncludes(f, "conditionExpression");
90
+ }
91
+ // The addressed default must land on the guard (which re-solicits the review), not escalation.
92
+ const addressed = flowElement("f_addressed");
93
+ assert(addressed && /targetRef="gw-guard"/.test(addressed), "default arm must re-enter gw-guard");
94
+ });
package/app/service.ts CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  fetchPrState,
18
18
  hasPendingCopilotReviewer,
19
19
  type MergeMethod,
20
+ type PrState,
20
21
  requestCopilotReview,
21
22
  } from "./github.ts";
22
23
  import { mergeLanes, readExclusions } from "./mergeExclusion.ts";
@@ -637,7 +638,7 @@ async function mirrorTaskStatusForPr(data: DataLayer, prKey: string, status: "op
637
638
  * • waiting_deps → every declared dependency has merged → `deps-cleared`
638
639
  * • waiting_merge → GitHub settled the PR as mergeable/blocked → `merge-ready` {mergeState}
639
640
  * • waiting_lane → predecessor in same exclusion lane merged → re-arm `waiting_merge`
640
- * • queued → the queued PR has landed on GitHub → `merge-landed`
641
+ * • queued → the queued PR landed → `merge-landed`; or it conflicts (DIRTY) → `merge-evicted`
641
642
  * On publish we flip status to the transient `merging` (which no branch scans) so a slow pass
642
643
  * can't double-signal, exactly as `pollReviews` flips to `converging`; `flipToMergingThenPublish`
643
644
  * reverts the flip if the publish fails so a failed handoff can't wedge the PR. */
@@ -756,25 +757,60 @@ async function pollMerges(data: DataLayer, engine: EngineClient, token: string)
756
757
  }
757
758
  }
758
759
 
759
- // 4) Queued PR landed?
760
+ // 4) Queued PR landed — or fell out of the merge queue?
761
+ //
762
+ // A PR enqueued by `attempt-merge` (mergeStatus="queued") parks the process at `wait-landed`.
763
+ // Two things can end that wait: the queue lands the PR (→ `merge-landed`), or the PR is EVICTED
764
+ // from the queue because its base moved under it and it now conflicts (`DIRTY`). Without the
765
+ // eviction path a conflicted-after-enqueue PR waits forever (nothing ever publishes
766
+ // `merge-landed`), which is exactly how #727/instance 729 wedged. We must NOT treat a merely
767
+ // "not yet landed" PR as evicted: while it is legitimately queuing GitHub often reports it as
768
+ // BLOCKED (a pending queue check) or UNSTABLE, which `classifyMergeability` calls not-ready. Only
769
+ // a genuine merge CONFLICT (`DIRTY`) means it has dropped out — evict on that alone and re-arm the
770
+ // merge poller (`merge-evicted` → `arm-merge`), which re-runs the mergeable gate so the existing
771
+ // auto-rebase / escalate machinery resolves the conflict.
760
772
  for (const pr of await prs(data).find({ status: "queued" })) {
761
773
  const { repo, number, pr_key: prKey } = pr;
762
774
  try {
763
775
  const st = await fetchPrState(repo, number, token);
764
776
  if (st === null) continue; // no transport → skip this PR (others may still advance)
765
- if (!st.merged) continue; // still in the queue
766
- await flipToMergingThenPublish(data, engine, prKey, "queued", {
767
- name: "merge-landed",
768
- correlationKey: prKey,
769
- variables: {},
770
- });
771
- console.log(`[poller] queued PR landed -> ${prKey}`);
777
+ const verdict = queuedVerdict(st);
778
+ if (verdict === "landed") {
779
+ await flipToMergingThenPublish(data, engine, prKey, "queued", {
780
+ name: "merge-landed",
781
+ correlationKey: prKey,
782
+ variables: {},
783
+ });
784
+ console.log(`[poller] queued PR landed -> ${prKey}`);
785
+ } else if (verdict === "evicted") {
786
+ await flipToMergingThenPublish(data, engine, prKey, "queued", {
787
+ name: "merge-evicted",
788
+ correlationKey: prKey,
789
+ variables: {},
790
+ });
791
+ console.log(`[poller] queued PR evicted (conflict) -> ${prKey}`);
792
+ }
793
+ // otherwise: still legitimately in the queue — keep waiting.
772
794
  } catch (err) {
773
795
  console.error(`[poller] queued ${prKey}: ${err}`);
774
796
  }
775
797
  }
776
798
  }
777
799
 
800
+ /** Decide what to do with a PR the process enqueued (parked at `wait-landed`), from its current
801
+ * GitHub merge state:
802
+ * • `landed` — the queue merged it → publish `merge-landed` (advance to mark-merged).
803
+ * • `evicted` — it fell out of the queue with a real merge CONFLICT (`DIRTY`) → publish
804
+ * `merge-evicted` so the process re-arms the merge poller and the mergeable gate re-runs
805
+ * (auto-rebase / escalate). Without this a conflicted-after-enqueue PR waits forever.
806
+ * • `waiting` — still legitimately in the queue. Crucially, a queuing PR is frequently reported
807
+ * BLOCKED/UNSTABLE (a pending queue check), which is NOT eviction — only `DIRTY` is. */
808
+ export function queuedVerdict(st: PrState): "landed" | "evicted" | "waiting" {
809
+ if (st.merged) return "landed";
810
+ if (st.mergeStateStatus === "DIRTY") return "evicted";
811
+ return "waiting";
812
+ }
813
+
778
814
  /** The subset of a Camunda-8 `/v2/jobs/search` result item this app reads. `worker` is the
779
815
  * leasing worker's name (empty/absent until an agent activates the job); `deadline` is the
780
816
  * activation lock's expiry (ISO ts). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/nano-workforce",
3
- "version": "0.40.0",
3
+ "version": "0.40.2",
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",
@@ -58,7 +58,11 @@ Because several agents may run on the same host at once:
58
58
  3. **Act.** Make the code changes for all fixes + nitpicks in your workspace (`cwd`)
59
59
  in one coherent, signed-off commit (`git commit -s`). Run the repo's
60
60
  build/test/lint locally before pushing. Push to the PR's head branch (the branch
61
- you are already on) — do not open a new branch or PR.
61
+ you are already on) — do not open a new branch or PR. If the branch has drifted
62
+ behind its base and you need to **rebase / resolve a merge conflict** to keep it
63
+ mergeable, that is allowed: do it in place on this branch and **force-push**
64
+ (`--force-with-lease`). Any push this round — including a rebase/force-push with
65
+ no reviewer comments to act on — is an **`addressed`** round (see the return table).
62
66
  4. **Reply in-thread** to each comment you addressed or pushed back on, one reply
63
67
  per comment, so the trail lives on the PR.
64
68
  5. **Resolve the thread** for every comment you handled — every *fix*, *nitpick*,
@@ -128,7 +132,7 @@ Return **one** of:
128
132
  | `status` | when | also set |
129
133
  |---------------|---------------------------------------------------------------|-----------------|
130
134
  | `converged` | nothing actionable left (see above) | `summary` |
131
- | `addressed` | you made changes + pushed this round | `summary` |
135
+ | `addressed` | you pushed anything this round code fixes, nitpicks, **or** a rebase/force-push to resolve a conflict | `summary` |
132
136
  | `waiting` | nothing to triage yet — you are awaiting a pending review (typically round 1) | `summary` |
133
137
  | `needs_input` | you hit a decision only a human can make | `summary`, `question` |
134
138
  | `blocked` | you are stuck on something external (auth, failing push, missing secret) | `summary`, `question` |
@@ -143,7 +147,8 @@ Never guess on a `needs_input` decision — raise it and let a human answer.
143
147
 
144
148
  Your result variables only reach the process if you emit them through the harness's
145
149
  result channel. Prose in your normal output is **not** parsed — if you only "say"
146
- your status in the transcript, the round escalates with an empty question. So:
150
+ your status in the transcript, the process can't read it, falls back to a safe
151
+ default, and you waste a round. So emit a machine-readable result one of two ways:
147
152
 
148
153
  1. **Write a JSON object to the file at `$AGENT_RESULT_FILE`** (an env var the
149
154
  harness sets for you). The object's keys become process variables. Example for a
@@ -169,3 +174,12 @@ your status in the transcript, the round escalates with an empty question. So:
169
174
  Do not put the result file inside the repo checkout or `git add` it — it lives
170
175
  outside your workspace. Exit `0` for every status (including `blocked`/`needs_input`);
171
176
  a non-zero exit means a genuine crash and the job is retried.
177
+
178
+ **Emitting a machine-readable result is your mandatory final step — never exit
179
+ silently.** Emitting a result (the `$AGENT_RESULT_FILE` write, or the stdout fallback
180
+ above if you truly cannot write the file) is the last thing you do on every path out of
181
+ this round (including after a rebase/force-push, or when nothing needed doing). If you
182
+ are ever unsure which status applies and you are not blocked on a human decision,
183
+ return **`addressed`** (or **`waiting`** if you are still awaiting the first review) —
184
+ never leave without a result. A missing result is treated as a safe `addressed` and
185
+ re-enters the review wait, but relying on that instead of emitting one wastes a round.
@@ -90,7 +90,7 @@
90
90
  <bpmn:incoming>f_answerLoop</bpmn:incoming>
91
91
  <bpmn:outgoing>f_toStatus</bpmn:outgoing>
92
92
  </bpmn:serviceTask>
93
- <bpmn:exclusiveGateway id="gw-status" name="status?" default="f_escalate">
93
+ <bpmn:exclusiveGateway id="gw-status" name="status?" default="f_addressed">
94
94
  <bpmn:incoming>f_toStatus</bpmn:incoming>
95
95
  <bpmn:outgoing>f_converged</bpmn:outgoing>
96
96
  <bpmn:outgoing>f_addressed</bpmn:outgoing>
@@ -202,13 +202,13 @@
202
202
  <bpmn:sequenceFlow id="f_converged" name="converged" sourceRef="gw-status" targetRef="persist-converged">
203
203
  <bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=status = "converged"</bpmn:conditionExpression>
204
204
  </bpmn:sequenceFlow>
205
- <bpmn:sequenceFlow id="f_addressed" name="addressed" sourceRef="gw-status" targetRef="gw-guard">
206
- <bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=status = "addressed"</bpmn:conditionExpression>
207
- </bpmn:sequenceFlow>
205
+ <bpmn:sequenceFlow id="f_addressed" name="addressed / default" sourceRef="gw-status" targetRef="gw-guard" />
208
206
  <bpmn:sequenceFlow id="f_waiting" name="waiting" sourceRef="gw-status" targetRef="gw-guard">
209
207
  <bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=status = "waiting"</bpmn:conditionExpression>
210
208
  </bpmn:sequenceFlow>
211
- <bpmn:sequenceFlow id="f_escalate" name="needs_input / blocked" sourceRef="gw-status" targetRef="persist-escalation" />
209
+ <bpmn:sequenceFlow id="f_escalate" name="needs_input / blocked" sourceRef="gw-status" targetRef="persist-escalation">
210
+ <bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=status = "needs_input" or status = "blocked"</bpmn:conditionExpression>
211
+ </bpmn:sequenceFlow>
212
212
  <bpmn:sequenceFlow id="f_guardMax" name="max rounds" sourceRef="gw-guard" targetRef="persist-escalation-maxrounds">
213
213
  <bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=round &gt;= maxRounds</bpmn:conditionExpression>
214
214
  </bpmn:sequenceFlow>
@@ -337,7 +337,7 @@
337
337
  <di:waypoint x="441" y="280" />
338
338
  <di:waypoint x="591" y="280" />
339
339
  <bpmndi:BPMNLabel>
340
- <dc:Bounds x="446" y="206" width="67" height="14" />
340
+ <dc:Bounds x="446" y="199" width="78" height="28" />
341
341
  </bpmndi:BPMNLabel>
342
342
  </bpmndi:BPMNEdge>
343
343
  <bpmndi:BPMNEdge id="BPMNEdge_f_waiting" bpmnElement="f_waiting">
@@ -18,6 +18,11 @@
18
18
  <zeebe:subscription correlationKey="=prKey" />
19
19
  </bpmn:extensionElements>
20
20
  </bpmn:message>
21
+ <bpmn:message id="Message_mergeEvicted" name="merge-evicted">
22
+ <bpmn:extensionElements>
23
+ <zeebe:subscription correlationKey="=prKey" />
24
+ </bpmn:extensionElements>
25
+ </bpmn:message>
21
26
  <bpmn:message id="Message_mergeEscAnswered" name="escalation-answered">
22
27
  <bpmn:extensionElements>
23
28
  <zeebe:subscription correlationKey="=prKey" />
@@ -109,6 +114,7 @@
109
114
  <bpmn:incoming>f_m_answer</bpmn:incoming>
110
115
  <bpmn:incoming>f_ci_fixed</bpmn:incoming>
111
116
  <bpmn:incoming>f_reb_rebased</bpmn:incoming>
117
+ <bpmn:incoming>f_m_evicted</bpmn:incoming>
112
118
  <bpmn:outgoing>f_m_arm</bpmn:outgoing>
113
119
  </bpmn:serviceTask>
114
120
  <bpmn:intermediateCatchEvent id="wait-mergeable" name="Wait: mergeable">
@@ -140,11 +146,21 @@
140
146
  <bpmn:outgoing>f_m_gQueued</bpmn:outgoing>
141
147
  <bpmn:outgoing>f_m_gBlocked</bpmn:outgoing>
142
148
  </bpmn:exclusiveGateway>
143
- <bpmn:intermediateCatchEvent id="wait-landed" name="Wait: merge queue landed">
149
+ <bpmn:eventBasedGateway id="eg-landed" name="landed or evicted?">
144
150
  <bpmn:incoming>f_m_gQueued</bpmn:incoming>
151
+ <bpmn:outgoing>f_eg_landed</bpmn:outgoing>
152
+ <bpmn:outgoing>f_eg_evicted</bpmn:outgoing>
153
+ </bpmn:eventBasedGateway>
154
+ <bpmn:intermediateCatchEvent id="wait-landed" name="Wait: merge queue landed">
155
+ <bpmn:incoming>f_eg_landed</bpmn:incoming>
145
156
  <bpmn:outgoing>f_m_landed</bpmn:outgoing>
146
157
  <bpmn:messageEventDefinition id="med_mergeLanded" messageRef="Message_mergeLanded" />
147
158
  </bpmn:intermediateCatchEvent>
159
+ <bpmn:intermediateCatchEvent id="wait-evicted" name="Wait: evicted from queue">
160
+ <bpmn:incoming>f_eg_evicted</bpmn:incoming>
161
+ <bpmn:outgoing>f_m_evicted</bpmn:outgoing>
162
+ <bpmn:messageEventDefinition id="med_mergeEvicted" messageRef="Message_mergeEvicted" />
163
+ </bpmn:intermediateCatchEvent>
148
164
  <bpmn:serviceTask id="mark-merged" name="Mark merged">
149
165
  <bpmn:extensionElements>
150
166
  <zeebe:taskDefinition type="pr.mark-merged" />
@@ -287,9 +303,12 @@
287
303
  <bpmn:sequenceFlow id="f_m_gMerged" name="merged" sourceRef="gw-merge" targetRef="mark-merged">
288
304
  <bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=mergeStatus = "merged"</bpmn:conditionExpression>
289
305
  </bpmn:sequenceFlow>
290
- <bpmn:sequenceFlow id="f_m_gQueued" name="queued" sourceRef="gw-merge" targetRef="wait-landed">
306
+ <bpmn:sequenceFlow id="f_m_gQueued" name="queued" sourceRef="gw-merge" targetRef="eg-landed">
291
307
  <bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=mergeStatus = "queued"</bpmn:conditionExpression>
292
308
  </bpmn:sequenceFlow>
309
+ <bpmn:sequenceFlow id="f_eg_landed" sourceRef="eg-landed" targetRef="wait-landed" />
310
+ <bpmn:sequenceFlow id="f_eg_evicted" sourceRef="eg-landed" targetRef="wait-evicted" />
311
+ <bpmn:sequenceFlow id="f_m_evicted" sourceRef="wait-evicted" targetRef="arm-merge" />
293
312
  <bpmn:sequenceFlow id="f_m_gBlocked" name="blocked" sourceRef="gw-merge" targetRef="merge-esc-attempt" />
294
313
  <bpmn:sequenceFlow id="f_m_landed" sourceRef="wait-landed" targetRef="mark-merged" />
295
314
  <bpmn:sequenceFlow id="f_m_done" sourceRef="mark-merged" targetRef="MergeEnd" />
@@ -332,49 +351,61 @@
332
351
  <bpmndi:BPMNShape id="BPMNShape_gw-merge" bpmnElement="gw-merge" isMarkerVisible="true">
333
352
  <dc:Bounds x="1063" y="95" width="50" height="50" />
334
353
  <bpmndi:BPMNLabel>
335
- <dc:Bounds x="1062" y="62" width="53" height="28" />
354
+ <dc:Bounds x="1022" y="150" width="53" height="28" />
355
+ </bpmndi:BPMNLabel>
356
+ </bpmndi:BPMNShape>
357
+ <bpmndi:BPMNShape id="BPMNShape_eg-landed" bpmnElement="eg-landed" isMarkerVisible="true">
358
+ <dc:Bounds x="1238" y="255" width="50" height="50" />
359
+ <bpmndi:BPMNLabel>
360
+ <dc:Bounds x="1231" y="222" width="64" height="28" />
336
361
  </bpmndi:BPMNLabel>
337
362
  </bpmndi:BPMNShape>
338
363
  <bpmndi:BPMNShape id="BPMNShape_wait-landed" bpmnElement="wait-landed">
339
- <dc:Bounds x="1245" y="262" width="36" height="36" />
364
+ <dc:Bounds x="1420" y="262" width="36" height="36" />
340
365
  <bpmndi:BPMNLabel>
341
- <dc:Bounds x="1221" y="229" width="85" height="28" />
366
+ <dc:Bounds x="1396" y="303" width="85" height="28" />
367
+ </bpmndi:BPMNLabel>
368
+ </bpmndi:BPMNShape>
369
+ <bpmndi:BPMNShape id="BPMNShape_wait-evicted" bpmnElement="wait-evicted">
370
+ <dc:Bounds x="1420" y="422" width="36" height="36" />
371
+ <bpmndi:BPMNLabel>
372
+ <dc:Bounds x="1394" y="375" width="88" height="42" />
342
373
  </bpmndi:BPMNLabel>
343
374
  </bpmndi:BPMNShape>
344
375
  <bpmndi:BPMNShape id="BPMNShape_mark-merged" bpmnElement="mark-merged">
345
- <dc:Bounds x="1388" y="80" width="100" height="80" />
376
+ <dc:Bounds x="1588" y="80" width="100" height="80" />
346
377
  </bpmndi:BPMNShape>
347
378
  <bpmndi:BPMNShape id="BPMNShape_MergeEnd" bpmnElement="MergeEnd">
348
- <dc:Bounds x="1588" y="102" width="36" height="36" />
379
+ <dc:Bounds x="1788" y="102" width="36" height="36" />
349
380
  <bpmndi:BPMNLabel>
350
- <dc:Bounds x="1581" y="143" width="50" height="14" />
381
+ <dc:Bounds x="1781" y="143" width="50" height="14" />
351
382
  </bpmndi:BPMNLabel>
352
383
  </bpmndi:BPMNShape>
353
384
  <bpmndi:BPMNShape id="BPMNShape_merge-esc-conflict" bpmnElement="merge-esc-conflict">
354
- <dc:Bounds x="1038" y="560" width="100" height="80" />
385
+ <dc:Bounds x="1038" y="720" width="100" height="80" />
355
386
  </bpmndi:BPMNShape>
356
387
  <bpmndi:BPMNShape id="BPMNShape_merge-esc-attempt" bpmnElement="merge-esc-attempt">
357
- <dc:Bounds x="1388" y="400" width="100" height="80" />
388
+ <dc:Bounds x="1388" y="560" width="100" height="80" />
358
389
  </bpmndi:BPMNShape>
359
390
  <bpmndi:BPMNShape id="BPMNShape_wait-merge-answer" bpmnElement="wait-merge-answer">
360
- <dc:Bounds x="1588" y="422" width="36" height="36" />
391
+ <dc:Bounds x="1620" y="582" width="36" height="36" />
361
392
  <bpmndi:BPMNLabel>
362
- <dc:Bounds x="1649" y="463" width="74" height="42" />
393
+ <dc:Bounds x="1661" y="579" width="74" height="42" />
363
394
  </bpmndi:BPMNLabel>
364
395
  </bpmndi:BPMNShape>
365
396
  <bpmndi:BPMNShape id="BPMNShape_gw-ci-fix" bpmnElement="gw-ci-fix" isMarkerVisible="true">
366
- <dc:Bounds x="863" y="575" width="50" height="50" />
397
+ <dc:Bounds x="863" y="735" width="50" height="50" />
367
398
  <bpmndi:BPMNLabel>
368
- <dc:Bounds x="844" y="556" width="89" height="14" />
399
+ <dc:Bounds x="844" y="716" width="89" height="14" />
369
400
  </bpmndi:BPMNLabel>
370
401
  </bpmndi:BPMNShape>
371
402
  <bpmndi:BPMNShape id="BPMNShape_fix-ci" bpmnElement="fix-ci">
372
- <dc:Bounds x="1038" y="720" width="100" height="80" />
403
+ <dc:Bounds x="1038" y="880" width="100" height="80" />
373
404
  </bpmndi:BPMNShape>
374
405
  <bpmndi:BPMNShape id="BPMNShape_gw-ci-result" bpmnElement="gw-ci-result" isMarkerVisible="true">
375
- <dc:Bounds x="1238" y="735" width="50" height="50" />
406
+ <dc:Bounds x="1238" y="895" width="50" height="50" />
376
407
  <bpmndi:BPMNLabel>
377
- <dc:Bounds x="1240" y="716" width="46" height="14" />
408
+ <dc:Bounds x="1240" y="876" width="46" height="14" />
378
409
  </bpmndi:BPMNLabel>
379
410
  </bpmndi:BPMNShape>
380
411
  <bpmndi:BPMNShape id="BPMNShape_gw-rebase" bpmnElement="gw-rebase" isMarkerVisible="true">
@@ -384,12 +415,12 @@
384
415
  </bpmndi:BPMNLabel>
385
416
  </bpmndi:BPMNShape>
386
417
  <bpmndi:BPMNShape id="BPMNShape_rebase" bpmnElement="rebase">
387
- <dc:Bounds x="1038" y="880" width="100" height="80" />
418
+ <dc:Bounds x="1038" y="1040" width="100" height="80" />
388
419
  </bpmndi:BPMNShape>
389
420
  <bpmndi:BPMNShape id="BPMNShape_gw-rebase-result" bpmnElement="gw-rebase-result" isMarkerVisible="true">
390
- <dc:Bounds x="1238" y="895" width="50" height="50" />
421
+ <dc:Bounds x="1238" y="1055" width="50" height="50" />
391
422
  <bpmndi:BPMNLabel>
392
- <dc:Bounds x="1233" y="876" width="60" height="14" />
423
+ <dc:Bounds x="1233" y="1036" width="60" height="14" />
393
424
  </bpmndi:BPMNLabel>
394
425
  </bpmndi:BPMNShape>
395
426
  <bpmndi:BPMNEdge id="BPMNEdge_f_m_start" bpmnElement="f_m_start">
@@ -421,69 +452,78 @@
421
452
  </bpmndi:BPMNEdge>
422
453
  <bpmndi:BPMNEdge id="BPMNEdge_f_m_gMerged" bpmnElement="f_m_gMerged">
423
454
  <di:waypoint x="1113" y="120" />
424
- <di:waypoint x="1388" y="120" />
455
+ <di:waypoint x="1588" y="120" />
425
456
  <bpmndi:BPMNLabel>
426
- <dc:Bounds x="1216" y="98" width="49" height="14" />
457
+ <dc:Bounds x="1326" y="98" width="49" height="14" />
427
458
  </bpmndi:BPMNLabel>
428
459
  </bpmndi:BPMNEdge>
429
460
  <bpmndi:BPMNEdge id="BPMNEdge_f_m_done" bpmnElement="f_m_done">
430
- <di:waypoint x="1488" y="120" />
431
- <di:waypoint x="1588" y="120" />
461
+ <di:waypoint x="1688" y="120" />
462
+ <di:waypoint x="1788" y="120" />
432
463
  </bpmndi:BPMNEdge>
433
464
  <bpmndi:BPMNEdge id="BPMNEdge_f_ci_giveup" bpmnElement="f_ci_giveup">
434
- <di:waypoint x="913" y="600" />
435
- <di:waypoint x="1038" y="600" />
465
+ <di:waypoint x="913" y="760" />
466
+ <di:waypoint x="1038" y="760" />
436
467
  <bpmndi:BPMNLabel>
437
- <dc:Bounds x="942" y="605" width="67" height="28" />
468
+ <dc:Bounds x="942" y="765" width="67" height="28" />
438
469
  </bpmndi:BPMNLabel>
439
470
  </bpmndi:BPMNEdge>
440
471
  <bpmndi:BPMNEdge id="BPMNEdge_f_ci_blocked" bpmnElement="f_ci_blocked">
441
- <di:waypoint x="1288" y="760" />
442
- <di:waypoint x="1438" y="760" />
443
- <di:waypoint x="1438" y="480" />
472
+ <di:waypoint x="1288" y="920" />
473
+ <di:waypoint x="1438" y="920" />
474
+ <di:waypoint x="1438" y="640" />
444
475
  <bpmndi:BPMNLabel>
445
- <dc:Bounds x="1319" y="738" width="89" height="14" />
476
+ <dc:Bounds x="1319" y="898" width="89" height="14" />
446
477
  </bpmndi:BPMNLabel>
447
478
  </bpmndi:BPMNEdge>
448
479
  <bpmndi:BPMNEdge id="BPMNEdge_f_reb_giveup" bpmnElement="f_reb_giveup">
449
480
  <di:waypoint x="913" y="280" />
450
481
  <di:waypoint x="1088" y="280" />
451
- <di:waypoint x="1088" y="560" />
482
+ <di:waypoint x="1088" y="720" />
452
483
  <bpmndi:BPMNLabel>
453
484
  <dc:Bounds x="977" y="247" width="67" height="28" />
454
485
  </bpmndi:BPMNLabel>
455
486
  </bpmndi:BPMNEdge>
456
487
  <bpmndi:BPMNEdge id="BPMNEdge_f_reb_blocked" bpmnElement="f_reb_blocked">
457
- <di:waypoint x="1288" y="920" />
458
- <di:waypoint x="1438" y="920" />
459
- <di:waypoint x="1438" y="480" />
488
+ <di:waypoint x="1288" y="1080" />
489
+ <di:waypoint x="1438" y="1080" />
490
+ <di:waypoint x="1438" y="640" />
460
491
  <bpmndi:BPMNLabel>
461
- <dc:Bounds x="1331" y="887" width="64" height="28" />
492
+ <dc:Bounds x="1331" y="1047" width="64" height="28" />
462
493
  </bpmndi:BPMNLabel>
463
494
  </bpmndi:BPMNEdge>
495
+ <bpmndi:BPMNEdge id="BPMNEdge_f_eg_landed" bpmnElement="f_eg_landed">
496
+ <di:waypoint x="1288" y="280" />
497
+ <di:waypoint x="1420" y="280" />
498
+ </bpmndi:BPMNEdge>
499
+ <bpmndi:BPMNEdge id="BPMNEdge_f_m_landed" bpmnElement="f_m_landed">
500
+ <di:waypoint x="1456" y="280" />
501
+ <di:waypoint x="1638" y="280" />
502
+ <di:waypoint x="1638" y="160" />
503
+ </bpmndi:BPMNEdge>
464
504
  <bpmndi:BPMNEdge id="BPMNEdge_f_m_escC" bpmnElement="f_m_escC">
465
- <di:waypoint x="1138" y="580" />
466
- <di:waypoint x="1158" y="580" />
467
- <di:waypoint x="1158" y="380" />
468
- <di:waypoint x="1606" y="380" />
469
- <di:waypoint x="1606" y="422" />
505
+ <di:waypoint x="1138" y="740" />
506
+ <di:waypoint x="1158" y="740" />
507
+ <di:waypoint x="1158" y="540" />
508
+ <di:waypoint x="1638" y="540" />
509
+ <di:waypoint x="1638" y="582" />
470
510
  </bpmndi:BPMNEdge>
471
511
  <bpmndi:BPMNEdge id="BPMNEdge_f_m_escA" bpmnElement="f_m_escA">
472
- <di:waypoint x="1488" y="440" />
473
- <di:waypoint x="1588" y="440" />
512
+ <di:waypoint x="1488" y="600" />
513
+ <di:waypoint x="1620" y="600" />
474
514
  </bpmndi:BPMNEdge>
475
515
  <bpmndi:BPMNEdge id="BPMNEdge_f_m_answer" bpmnElement="f_m_answer">
476
- <di:waypoint x="1606" y="458" />
477
- <di:waypoint x="1606" y="500" />
478
- <di:waypoint x="402" y="500" />
516
+ <di:waypoint x="1638" y="618" />
517
+ <di:waypoint x="1638" y="660" />
518
+ <di:waypoint x="402" y="660" />
479
519
  <di:waypoint x="402" y="160" />
480
520
  </bpmndi:BPMNEdge>
481
521
  <bpmndi:BPMNEdge id="BPMNEdge_f_m_mCiFix" bpmnElement="f_m_mCiFix">
482
522
  <di:waypoint x="713" y="145" />
483
- <di:waypoint x="713" y="600" />
484
- <di:waypoint x="863" y="600" />
523
+ <di:waypoint x="713" y="760" />
524
+ <di:waypoint x="863" y="760" />
485
525
  <bpmndi:BPMNLabel>
486
- <dc:Bounds x="718" y="438" width="60" height="42" />
526
+ <dc:Bounds x="718" y="518" width="60" height="42" />
487
527
  </bpmndi:BPMNLabel>
488
528
  </bpmndi:BPMNEdge>
489
529
  <bpmndi:BPMNEdge id="BPMNEdge_f_m_mRebase" bpmnElement="f_m_mRebase">
@@ -498,18 +538,18 @@
498
538
  <di:waypoint x="713" y="145" />
499
539
  <di:waypoint x="713" y="318" />
500
540
  <di:waypoint x="1018" y="318" />
501
- <di:waypoint x="1018" y="580" />
502
- <di:waypoint x="1038" y="580" />
541
+ <di:waypoint x="1018" y="740" />
542
+ <di:waypoint x="1038" y="740" />
503
543
  <bpmndi:BPMNLabel>
504
544
  <dc:Bounds x="823" y="326" width="85" height="14" />
505
545
  </bpmndi:BPMNLabel>
506
546
  </bpmndi:BPMNEdge>
507
547
  <bpmndi:BPMNEdge id="BPMNEdge_f_ci_go" bpmnElement="f_ci_go">
508
- <di:waypoint x="888" y="625" />
509
- <di:waypoint x="888" y="760" />
510
- <di:waypoint x="1038" y="760" />
548
+ <di:waypoint x="888" y="785" />
549
+ <di:waypoint x="888" y="920" />
550
+ <di:waypoint x="1038" y="920" />
511
551
  <bpmndi:BPMNLabel>
512
- <dc:Bounds x="893" y="679" width="49" height="28" />
552
+ <dc:Bounds x="893" y="839" width="49" height="28" />
513
553
  </bpmndi:BPMNLabel>
514
554
  </bpmndi:BPMNEdge>
515
555
  <bpmndi:BPMNEdge id="BPMNEdge_f_reb_go" bpmnElement="f_reb_go">
@@ -517,8 +557,8 @@
517
557
  <di:waypoint x="933" y="280" />
518
558
  <di:waypoint x="933" y="305" />
519
559
  <di:waypoint x="1158" y="305" />
520
- <di:waypoint x="1158" y="920" />
521
- <di:waypoint x="1138" y="920" />
560
+ <di:waypoint x="1158" y="1080" />
561
+ <di:waypoint x="1138" y="1080" />
522
562
  <bpmndi:BPMNLabel>
523
563
  <dc:Bounds x="1101" y="310" width="49" height="28" />
524
564
  </bpmndi:BPMNLabel>
@@ -526,60 +566,64 @@
526
566
  <bpmndi:BPMNEdge id="BPMNEdge_f_m_gQueued" bpmnElement="f_m_gQueued">
527
567
  <di:waypoint x="1088" y="145" />
528
568
  <di:waypoint x="1088" y="280" />
529
- <di:waypoint x="1245" y="280" />
569
+ <di:waypoint x="1238" y="280" />
530
570
  <bpmndi:BPMNLabel>
531
571
  <dc:Bounds x="1093" y="206" width="46" height="14" />
532
572
  </bpmndi:BPMNLabel>
533
573
  </bpmndi:BPMNEdge>
574
+ <bpmndi:BPMNEdge id="BPMNEdge_f_eg_evicted" bpmnElement="f_eg_evicted">
575
+ <di:waypoint x="1263" y="305" />
576
+ <di:waypoint x="1263" y="440" />
577
+ <di:waypoint x="1420" y="440" />
578
+ </bpmndi:BPMNEdge>
534
579
  <bpmndi:BPMNEdge id="BPMNEdge_f_m_gBlocked" bpmnElement="f_m_gBlocked">
535
- <di:waypoint x="1113" y="120" />
536
- <di:waypoint x="1368" y="120" />
537
- <di:waypoint x="1368" y="440" />
538
- <di:waypoint x="1388" y="440" />
580
+ <di:waypoint x="1088" y="95" />
581
+ <di:waypoint x="1088" y="75" />
582
+ <di:waypoint x="1844" y="75" />
583
+ <di:waypoint x="1844" y="478" />
584
+ <di:waypoint x="1368" y="478" />
585
+ <di:waypoint x="1368" y="580" />
586
+ <di:waypoint x="1388" y="580" />
539
587
  <bpmndi:BPMNLabel>
540
- <dc:Bounds x="1373" y="273" width="53" height="14" />
588
+ <dc:Bounds x="1849" y="270" width="53" height="14" />
541
589
  </bpmndi:BPMNLabel>
542
590
  </bpmndi:BPMNEdge>
543
591
  <bpmndi:BPMNEdge id="BPMNEdge_f_ci_done" bpmnElement="f_ci_done">
544
- <di:waypoint x="1138" y="760" />
545
- <di:waypoint x="1238" y="760" />
592
+ <di:waypoint x="1138" y="920" />
593
+ <di:waypoint x="1238" y="920" />
546
594
  </bpmndi:BPMNEdge>
547
595
  <bpmndi:BPMNEdge id="BPMNEdge_f_reb_done" bpmnElement="f_reb_done">
548
- <di:waypoint x="1138" y="940" />
549
- <di:waypoint x="1158" y="940" />
550
- <di:waypoint x="1158" y="965" />
551
- <di:waypoint x="1238" y="965" />
552
- <di:waypoint x="1263" y="965" />
553
- <di:waypoint x="1263" y="945" />
554
- </bpmndi:BPMNEdge>
555
- <bpmndi:BPMNEdge id="BPMNEdge_f_m_landed" bpmnElement="f_m_landed">
556
- <di:waypoint x="1263" y="298" />
557
- <di:waypoint x="1263" y="318" />
558
- <di:waypoint x="1281" y="318" />
559
- <di:waypoint x="1281" y="540" />
560
- <di:waypoint x="1644" y="540" />
561
- <di:waypoint x="1644" y="180" />
562
- <di:waypoint x="1468" y="180" />
563
- <di:waypoint x="1468" y="160" />
596
+ <di:waypoint x="1138" y="1100" />
597
+ <di:waypoint x="1158" y="1100" />
598
+ <di:waypoint x="1158" y="1125" />
599
+ <di:waypoint x="1238" y="1125" />
600
+ <di:waypoint x="1263" y="1125" />
601
+ <di:waypoint x="1263" y="1105" />
564
602
  </bpmndi:BPMNEdge>
565
603
  <bpmndi:BPMNEdge id="BPMNEdge_f_ci_fixed" bpmnElement="f_ci_fixed">
566
- <di:waypoint x="1263" y="785" />
567
- <di:waypoint x="1263" y="820" />
568
- <di:waypoint x="402" y="820" />
604
+ <di:waypoint x="1263" y="945" />
605
+ <di:waypoint x="1263" y="980" />
606
+ <di:waypoint x="402" y="980" />
569
607
  <di:waypoint x="402" y="160" />
570
608
  <bpmndi:BPMNLabel>
571
- <dc:Bounds x="813" y="798" width="39" height="14" />
609
+ <dc:Bounds x="813" y="958" width="39" height="14" />
572
610
  </bpmndi:BPMNLabel>
573
611
  </bpmndi:BPMNEdge>
574
612
  <bpmndi:BPMNEdge id="BPMNEdge_f_reb_rebased" bpmnElement="f_reb_rebased">
575
- <di:waypoint x="1263" y="945" />
576
- <di:waypoint x="1263" y="980" />
577
- <di:waypoint x="402" y="980" />
613
+ <di:waypoint x="1263" y="1105" />
614
+ <di:waypoint x="1263" y="1140" />
615
+ <di:waypoint x="402" y="1140" />
578
616
  <di:waypoint x="402" y="160" />
579
617
  <bpmndi:BPMNLabel>
580
- <dc:Bounds x="806" y="958" width="53" height="14" />
618
+ <dc:Bounds x="806" y="1118" width="53" height="14" />
581
619
  </bpmndi:BPMNLabel>
582
620
  </bpmndi:BPMNEdge>
621
+ <bpmndi:BPMNEdge id="BPMNEdge_f_m_evicted" bpmnElement="f_m_evicted">
622
+ <di:waypoint x="1438" y="458" />
623
+ <di:waypoint x="1438" y="478" />
624
+ <di:waypoint x="402" y="478" />
625
+ <di:waypoint x="402" y="160" />
626
+ </bpmndi:BPMNEdge>
583
627
  </bpmndi:BPMNPlane>
584
628
  </bpmndi:BPMNDiagram>
585
629
  </bpmn:definitions>