@nanobpm/nano-workforce 0.201.1 → 0.201.3
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 +12 -0
- package/app/agentActivationLedger.test.ts +109 -0
- package/app/agentActivationLedger.ts +144 -0
- package/app/agentActivationsPoll.test.ts +180 -0
- package/app/agentSlaEscalation.test.ts +137 -0
- package/app/agentSlaEscalation.ts +169 -0
- package/app/agentSlaMarkerGuard.test.ts +172 -0
- package/app/contracts.ts +2 -2
- package/app/deliveryRunner.test.ts +14 -11
- package/app/feature.test.ts +5 -3
- package/app/mergeLoopBehaviour.test.ts +46 -1
- package/app/plan.test.ts +5 -2
- package/app/repoEnvelope.test.ts +139 -0
- package/app/repoEnvelope.ts +24 -15
- package/app/service.test.ts +21 -16
- package/app/service.ts +94 -0
- package/db/migrations/119_agent_job_activations.sql +50 -0
- package/e2e/agent-sla-boundary.e2e.ts +51 -8
- package/e2e/feature-run.e2e.ts +6 -4
- package/e2e/plan-fanout.e2e.ts +6 -4
- package/package.json +1 -1
- package/resources/processes/convergence-loop.bpmn +10 -0
- package/resources/processes/implement-cell.bpmn +4 -1
- package/resources/processes/merge-cell.bpmn +4 -1
- package/resources/processes/merge-loop.bpmn +7 -0
- package/resources/processes/plan-fanout.bpmn +5 -0
- package/resources/processes/retro.bpmn +5 -2
- package/test/derivation-parity/flows.ts +5 -3
- package/workers/conformance-record/worker.ts +15 -1
- package/workers/persist-escalation/worker.ts +22 -3
- package/workers/record-feature-escalation/worker.ts +28 -12
- package/workers/record-trial-merge/worker.ts +16 -1
|
@@ -80,6 +80,8 @@
|
|
|
80
80
|
<nano:extend name="prNumber" type="integer" optional="true" />
|
|
81
81
|
<nano:extend name="prUrl" type="string" optional="true" />
|
|
82
82
|
<nano:extend name="abandonUrl" type="string" optional="true" />
|
|
83
|
+
<nano:extend name="agentSlaJobType" type="string" optional="true" />
|
|
84
|
+
<nano:extend name="agentSlaTimeout" type="string" optional="true" />
|
|
83
85
|
</nano:shape>
|
|
84
86
|
<nano:shape id="EscalationOut" name="Record escalation — result">
|
|
85
87
|
<nano:extend name="escalationId" type="integer" optional="true" />
|
|
@@ -268,6 +270,7 @@
|
|
|
268
270
|
<zeebe:input source="="blocked"" target="status" />
|
|
269
271
|
<zeebe:input source="=false" target="recordRound" />
|
|
270
272
|
<zeebe:input source="=if (noProgressQuestion != null and trim(noProgressQuestion) != "") then noProgressQuestion else "Round " + string(round) + " reported the review comments were addressed, but the PR head did not advance (no commit was pushed), so another review round would loop on identical code. A human must decide how to proceed (reply to resume the loop)."" target="question" />
|
|
273
|
+
<zeebe:input source="=null" target="agentSlaJobType" />
|
|
271
274
|
</zeebe:ioMapping>
|
|
272
275
|
</bpmn:extensionElements>
|
|
273
276
|
<bpmn:incoming>f_huskEscalate</bpmn:incoming>
|
|
@@ -307,6 +310,7 @@
|
|
|
307
310
|
<zeebe:input source="="blocked"" target="status" />
|
|
308
311
|
<zeebe:input source="=false" target="recordRound" />
|
|
309
312
|
<zeebe:input source="="No review arrived within the review-wait timeout (" + reviewWaitTimeout + "). A human must decide how to proceed (reply to resume the loop)."" target="question" />
|
|
313
|
+
<zeebe:input source="=null" target="agentSlaJobType" />
|
|
310
314
|
</zeebe:ioMapping>
|
|
311
315
|
</bpmn:extensionElements>
|
|
312
316
|
<bpmn:incoming>f_reviewStalled</bpmn:incoming>
|
|
@@ -319,6 +323,9 @@
|
|
|
319
323
|
<zeebe:property name="io.nanobpm.dataEnvelope.in" value="EscalationIn" />
|
|
320
324
|
<zeebe:property name="io.nanobpm.dataEnvelope.out" value="EscalationOut" />
|
|
321
325
|
</zeebe:properties>
|
|
326
|
+
<zeebe:ioMapping>
|
|
327
|
+
<zeebe:input source="=null" target="agentSlaJobType" />
|
|
328
|
+
</zeebe:ioMapping>
|
|
322
329
|
</bpmn:extensionElements>
|
|
323
330
|
<bpmn:incoming>f_escalate</bpmn:incoming>
|
|
324
331
|
<bpmn:outgoing>f_escGate</bpmn:outgoing>
|
|
@@ -344,6 +351,7 @@
|
|
|
344
351
|
<zeebe:input source="="blocked"" target="status" />
|
|
345
352
|
<zeebe:input source="=false" target="recordRound" />
|
|
346
353
|
<zeebe:input source="="Not converged after " + string(maxRounds) + " rounds. A human must decide how to proceed."" target="question" />
|
|
354
|
+
<zeebe:input source="=null" target="agentSlaJobType" />
|
|
347
355
|
</zeebe:ioMapping>
|
|
348
356
|
</bpmn:extensionElements>
|
|
349
357
|
<bpmn:incoming>f_guardMax</bpmn:incoming>
|
|
@@ -452,6 +460,7 @@
|
|
|
452
460
|
<zeebe:input source="="blocked"" target="status" />
|
|
453
461
|
<zeebe:input source="=false" target="recordRound" />
|
|
454
462
|
<zeebe:input source="=convergeBlockReason" target="question" />
|
|
463
|
+
<zeebe:input source="=null" target="agentSlaJobType" />
|
|
455
464
|
</zeebe:ioMapping>
|
|
456
465
|
</bpmn:extensionElements>
|
|
457
466
|
<bpmn:incoming>f_ackEscalate</bpmn:incoming>
|
|
@@ -491,6 +500,7 @@
|
|
|
491
500
|
<zeebe:input source="="blocked"" target="status" />
|
|
492
501
|
<zeebe:input source="=false" target="recordRound" />
|
|
493
502
|
<zeebe:input source="=scopeBlockReason" target="question" />
|
|
503
|
+
<zeebe:input source="=null" target="agentSlaJobType" />
|
|
494
504
|
<zeebe:output source="=true" target="scopePending" />
|
|
495
505
|
</zeebe:ioMapping>
|
|
496
506
|
</bpmn:extensionElements>
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
<nano:extend name="transcriptUrl" type="string" optional="true" />
|
|
12
12
|
<nano:extend name="pr" type="string" optional="true" />
|
|
13
13
|
<nano:extend name="deliveryVerified" type="boolean" optional="true" />
|
|
14
|
+
<nano:extend name="agentSlaJobType" type="string" optional="true" />
|
|
15
|
+
<nano:extend name="agentSlaTimeout" type="string" optional="true" />
|
|
14
16
|
</nano:shape>
|
|
15
17
|
</nano:shapes>
|
|
16
18
|
</bpmn:extensionElements>
|
|
@@ -91,6 +93,7 @@
|
|
|
91
93
|
<zeebe:property name="io.nanobpm.dataEnvelope.in" value="RecordEscalationIn" />
|
|
92
94
|
</zeebe:properties>
|
|
93
95
|
<zeebe:ioMapping>
|
|
96
|
+
<zeebe:input source="=null" target="agentSlaJobType" />
|
|
94
97
|
<zeebe:output source="=question" target="question" />
|
|
95
98
|
</zeebe:ioMapping>
|
|
96
99
|
</bpmn:extensionElements>
|
|
@@ -104,7 +107,7 @@
|
|
|
104
107
|
<zeebe:property name="io.nanobpm.dataEnvelope.in" value="RecordEscalationIn" />
|
|
105
108
|
</zeebe:properties>
|
|
106
109
|
<zeebe:ioMapping>
|
|
107
|
-
<zeebe:input source="="
|
|
110
|
+
<zeebe:input source="="senior:feature"" target="agentSlaJobType" />
|
|
108
111
|
<zeebe:output source="=question" target="question" />
|
|
109
112
|
</zeebe:ioMapping>
|
|
110
113
|
</bpmn:extensionElements>
|
|
@@ -36,6 +36,9 @@
|
|
|
36
36
|
<zeebe:properties>
|
|
37
37
|
<zeebe:property name="io.nanobpm.dataEnvelope.in" value="RecordTrialMergeIn" />
|
|
38
38
|
</zeebe:properties>
|
|
39
|
+
<zeebe:ioMapping>
|
|
40
|
+
<zeebe:input source="=null" target="agentSlaJobType" />
|
|
41
|
+
</zeebe:ioMapping>
|
|
39
42
|
</bpmn:extensionElements>
|
|
40
43
|
<bpmn:incoming>mc_toRecord</bpmn:incoming>
|
|
41
44
|
<bpmn:outgoing>mc_toGwTrial</bpmn:outgoing>
|
|
@@ -48,7 +51,7 @@
|
|
|
48
51
|
</zeebe:properties>
|
|
49
52
|
<zeebe:ioMapping>
|
|
50
53
|
<zeebe:input source="="suite-failed"" target="result" />
|
|
51
|
-
<zeebe:input source="="
|
|
54
|
+
<zeebe:input source="="senior:trial-merge"" target="agentSlaJobType" />
|
|
52
55
|
<zeebe:input source="=null" target="conflicts" />
|
|
53
56
|
<zeebe:input source="=null" target="failing" />
|
|
54
57
|
</zeebe:ioMapping>
|
|
@@ -110,6 +110,7 @@
|
|
|
110
110
|
<zeebe:ioMapping>
|
|
111
111
|
<zeebe:output source="=null" target="status" />
|
|
112
112
|
<zeebe:output source="=null" target="ciBlockedReconciled" />
|
|
113
|
+
<zeebe:output source="=null" target="agentSlaJobType" />
|
|
113
114
|
</zeebe:ioMapping>
|
|
114
115
|
</bpmn:extensionElements>
|
|
115
116
|
<bpmn:incoming>f_m_deps</bpmn:incoming>
|
|
@@ -302,6 +303,7 @@
|
|
|
302
303
|
<zeebe:ioMapping>
|
|
303
304
|
<zeebe:input source="="blocked"" target="status" />
|
|
304
305
|
<zeebe:input source="="This PR was reported as added to the merge queue but did not land within the merge-queue landing timeout (" + landedWaitTimeout + "). The repository may not have actually enqueued it — e.g. a Mergify repo where a plain merge does not queue and an explicit `@Mergifyio queue` is required, or a native merge queue that dropped it silently. Add the PR to the merge queue (or merge it) manually, then reply to retry."" target="question" />
|
|
306
|
+
<zeebe:input source="=null" target="agentSlaJobType" />
|
|
305
307
|
</zeebe:ioMapping>
|
|
306
308
|
</bpmn:extensionElements>
|
|
307
309
|
<bpmn:incoming>f_m_landedStalled</bpmn:incoming>
|
|
@@ -447,6 +449,7 @@
|
|
|
447
449
|
<bpmn:extensionElements>
|
|
448
450
|
<zeebe:ioMapping>
|
|
449
451
|
<zeebe:output source="="recheck"" target="ciOutcome" />
|
|
452
|
+
<zeebe:output source="=null" target="agentSlaJobType" />
|
|
450
453
|
</zeebe:ioMapping>
|
|
451
454
|
</bpmn:extensionElements>
|
|
452
455
|
<bpmn:incoming>f_cib_armed</bpmn:incoming>
|
|
@@ -463,6 +466,7 @@
|
|
|
463
466
|
<bpmn:extensionElements>
|
|
464
467
|
<zeebe:ioMapping>
|
|
465
468
|
<zeebe:output source="="escalate"" target="ciOutcome" />
|
|
469
|
+
<zeebe:output source="=null" target="agentSlaJobType" />
|
|
466
470
|
</zeebe:ioMapping>
|
|
467
471
|
</bpmn:extensionElements>
|
|
468
472
|
<bpmn:incoming>f_cib_esc</bpmn:incoming>
|
|
@@ -471,6 +475,7 @@
|
|
|
471
475
|
<bpmn:extensionElements>
|
|
472
476
|
<zeebe:ioMapping>
|
|
473
477
|
<zeebe:output source="="escalate"" target="ciOutcome" />
|
|
478
|
+
<zeebe:output source="="senior:fix-ci"" target="agentSlaJobType" />
|
|
474
479
|
</zeebe:ioMapping>
|
|
475
480
|
</bpmn:extensionElements>
|
|
476
481
|
<bpmn:incoming>f_ci_sla</bpmn:incoming>
|
|
@@ -578,6 +583,7 @@
|
|
|
578
583
|
<bpmn:extensionElements>
|
|
579
584
|
<zeebe:ioMapping>
|
|
580
585
|
<zeebe:output source="="escalate-attempt"" target="rebaseOutcome" />
|
|
586
|
+
<zeebe:output source="=null" target="agentSlaJobType" />
|
|
581
587
|
</zeebe:ioMapping>
|
|
582
588
|
</bpmn:extensionElements>
|
|
583
589
|
<bpmn:incoming>f_reb_blocked</bpmn:incoming>
|
|
@@ -586,6 +592,7 @@
|
|
|
586
592
|
<bpmn:extensionElements>
|
|
587
593
|
<zeebe:ioMapping>
|
|
588
594
|
<zeebe:output source="="escalate-conflict"" target="rebaseOutcome" />
|
|
595
|
+
<zeebe:output source="="senior:rebase"" target="agentSlaJobType" />
|
|
589
596
|
</zeebe:ioMapping>
|
|
590
597
|
</bpmn:extensionElements>
|
|
591
598
|
<bpmn:incoming>f_reb_sla</bpmn:incoming>
|
|
@@ -74,6 +74,8 @@
|
|
|
74
74
|
<nano:extend name="conflicts" type="string" list="true" optional="true" />
|
|
75
75
|
<nano:extend name="failing" type="string" list="true" optional="true" />
|
|
76
76
|
<nano:extend name="summary" type="string" optional="true" />
|
|
77
|
+
<nano:extend name="agentSlaJobType" type="string" optional="true" />
|
|
78
|
+
<nano:extend name="agentSlaTimeout" type="string" optional="true" />
|
|
77
79
|
</nano:shape>
|
|
78
80
|
<nano:shape id="RecordPlanReviewIn" name="Record plan review — input">
|
|
79
81
|
<nano:extend name="planKey" type="string" />
|
|
@@ -563,6 +565,9 @@
|
|
|
563
565
|
<zeebe:properties>
|
|
564
566
|
<zeebe:property name="io.nanobpm.dataEnvelope.in" value="RecordTrialMergeIn" />
|
|
565
567
|
</zeebe:properties>
|
|
568
|
+
<zeebe:ioMapping>
|
|
569
|
+
<zeebe:input source="=null" target="agentSlaJobType" />
|
|
570
|
+
</zeebe:ioMapping>
|
|
566
571
|
</bpmn:extensionElements>
|
|
567
572
|
<bpmn:incoming>f_toRecordTrialMerge</bpmn:incoming>
|
|
568
573
|
<bpmn:outgoing>f_toGwTrial</bpmn:outgoing>
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
<nano:extend name="hasDeviations" type="boolean" optional="true" />
|
|
26
26
|
<nano:extend name="agentSlaElapsed" type="boolean" optional="true" />
|
|
27
27
|
<nano:extend name="preserveConformance" type="boolean" optional="true" />
|
|
28
|
+
<nano:extend name="agentSlaJobType" type="string" optional="true" />
|
|
29
|
+
<nano:extend name="agentSlaTimeout" type="string" optional="true" />
|
|
28
30
|
<nano:extend name="summary" type="string" optional="true" />
|
|
29
31
|
</nano:shape>
|
|
30
32
|
</nano:shapes>
|
|
@@ -71,6 +73,7 @@
|
|
|
71
73
|
<zeebe:ioMapping>
|
|
72
74
|
<zeebe:input source="=false" target="agentSlaElapsed" />
|
|
73
75
|
<zeebe:input source="=false" target="preserveConformance" />
|
|
76
|
+
<zeebe:input source="=null" target="agentSlaJobType" />
|
|
74
77
|
</zeebe:ioMapping>
|
|
75
78
|
</bpmn:extensionElements>
|
|
76
79
|
<bpmn:incoming>f_toRecordConformance</bpmn:incoming>
|
|
@@ -85,7 +88,7 @@
|
|
|
85
88
|
<zeebe:ioMapping>
|
|
86
89
|
<zeebe:input source="=true" target="agentSlaElapsed" />
|
|
87
90
|
<zeebe:input source="=false" target="preserveConformance" />
|
|
88
|
-
<zeebe:input source="="
|
|
91
|
+
<zeebe:input source="="senior:conformance"" target="agentSlaJobType" />
|
|
89
92
|
</zeebe:ioMapping>
|
|
90
93
|
</bpmn:extensionElements>
|
|
91
94
|
<bpmn:incoming>f_conformanceSla</bpmn:incoming>
|
|
@@ -100,7 +103,7 @@
|
|
|
100
103
|
<zeebe:ioMapping>
|
|
101
104
|
<zeebe:input source="=true" target="agentSlaElapsed" />
|
|
102
105
|
<zeebe:input source="=true" target="preserveConformance" />
|
|
103
|
-
<zeebe:input source="="
|
|
106
|
+
<zeebe:input source="="senior:retro"" target="agentSlaJobType" />
|
|
104
107
|
</zeebe:ioMapping>
|
|
105
108
|
</bpmn:extensionElements>
|
|
106
109
|
<bpmn:incoming>f_synthesizeSla</bpmn:incoming>
|
|
@@ -137,13 +137,15 @@ export const retroFlow: DeclarativeFlow = defineFlow(
|
|
|
137
137
|
jobType: "pr.conformance-record",
|
|
138
138
|
// The golden pins both SLA-mode control flags to `=false` on this NORMAL-path recorder
|
|
139
139
|
// (round-7 review, PR #864): the flags share the external conformance agent's result
|
|
140
|
-
// namespace, so a hoisted agent output must never select the SLA/preserve branch.
|
|
141
|
-
//
|
|
142
|
-
// SLA
|
|
140
|
+
// namespace, so a hoisted agent output must never select the SLA/preserve branch. It also
|
|
141
|
+
// pins `agentSlaJobType` to `=null` (PR #881 review r4214170285) so a hoisted agent-supplied
|
|
142
|
+
// marker can never select the SLA escalation builder. The port reproduces those pins so the
|
|
143
|
+
// parity diagnostic's only residual is the class-3 marker + SLA boundaries.
|
|
143
144
|
io: {
|
|
144
145
|
input: [
|
|
145
146
|
{ source: "=false", target: "agentSlaElapsed" },
|
|
146
147
|
{ source: "=false", target: "preserveConformance" },
|
|
148
|
+
{ source: "=null", target: "agentSlaJobType" },
|
|
147
149
|
],
|
|
148
150
|
},
|
|
149
151
|
});
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// reads `status`/`pr`/`summary` through), and exposes the raw transcript under the
|
|
10
10
|
// `io.nanobpm.agentResult` envelope's `.output`.
|
|
11
11
|
import type { AppJobHandler } from "@nanobpm/urban";
|
|
12
|
+
import { agentSlaEscalationQuestion } from "../../app/agentSlaEscalation.ts";
|
|
12
13
|
import { markConformanceSynthesisSla, recordConformance } from "../../app/conformance.ts";
|
|
13
14
|
import type { WorkerInputs } from "../../nano-generated/worker-io.d.ts";
|
|
14
15
|
|
|
@@ -50,7 +51,20 @@ const handler: AppJobHandler<In> = async (job, app) => {
|
|
|
50
51
|
|
|
51
52
|
const commentUrl = asStr(job.variables.commentUrl);
|
|
52
53
|
const status = asStatus(job.variables.status, commentUrl !== null);
|
|
53
|
-
|
|
54
|
+
// Agent-task SLA path (issue #879): the conformance/synthesize agent's interrupting timer boundary
|
|
55
|
+
// routed here with `agentSlaJobType` set, so there is no agent-produced summary — build the accurate
|
|
56
|
+
// never-started-vs-hung reason (named job type + wait duration) from the durable activation ledger,
|
|
57
|
+
// replacing the old blanket "hung or looping" wording. Unset on the normal path, where the agent's
|
|
58
|
+
// own hoisted `summary` is used.
|
|
59
|
+
const slaJobType = asStr(job.variables.agentSlaJobType);
|
|
60
|
+
const summary = slaJobType
|
|
61
|
+
? await agentSlaEscalationQuestion(app.data, {
|
|
62
|
+
processInstanceKey: job.processInstanceKey != null ? String(job.processInstanceKey) : "",
|
|
63
|
+
jobType: slaJobType,
|
|
64
|
+
sla: asStr(job.variables.agentSlaTimeout) ?? "PT2H",
|
|
65
|
+
recovery: "Acknowledge to record the timeout and continue the retrospective.",
|
|
66
|
+
})
|
|
67
|
+
: asStr(job.variables.summary);
|
|
54
68
|
|
|
55
69
|
// biome-ignore lint/plugin: runtime/framework contract boundary for external data shape
|
|
56
70
|
const env = (job.variables as Record<string, unknown>)[AGENT_RESULT_KEY] as { output?: unknown } | undefined;
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
// a token on a durable wait that has no escalation for a human to answer.
|
|
19
19
|
import type { AppJobHandler } from "@nanobpm/urban";
|
|
20
20
|
import { abandonTokenFromUrl } from "../../app/abandon.ts";
|
|
21
|
+
import { agentSlaEscalationQuestion } from "../../app/agentSlaEscalation.ts";
|
|
21
22
|
import { classifyEscalation } from "../../app/escalationTaxonomy.ts";
|
|
22
23
|
import { ensurePr, parsePr } from "../../app/service.ts";
|
|
23
24
|
import type { WorkerInputs } from "../../nano-generated/worker-io.d.ts";
|
|
@@ -60,16 +61,34 @@ const handler: AppJobHandler<In> = async (job, app) => {
|
|
|
60
61
|
const status = rawStatus ?? "needs_input";
|
|
61
62
|
const transcript = transcriptOf(job.variables);
|
|
62
63
|
const worker = workerOf(job.variables);
|
|
64
|
+
// Agent-task SLA path (issue #879): the merge-loop's fix-ci / rebase interrupting SLA timer boundary
|
|
65
|
+
// routed here with `agentSlaJobType` set on the SLA end event. Rather than the old blanket "exceeded
|
|
66
|
+
// its time budget … check its progress" FEEL `else` (which misdiagnoses a never-activated,
|
|
67
|
+
// queue-starved job as hung), build the accurate never-started-vs-hung question — naming the job type
|
|
68
|
+
// and how long it waited — from the durable activation ledger. `agentSlaJobType` is set ONLY on the
|
|
69
|
+
// SLA arm, so it authoritatively selects this path; every other escalation (agent verdict, merge
|
|
70
|
+
// blocked, rebase conflict) leaves it unset and keeps its FEEL-built `question`. It is CLEARED on
|
|
71
|
+
// every return below so a later non-SLA escalation in the same looping instance can never inherit a
|
|
72
|
+
// stale value.
|
|
73
|
+
const slaJobType = nonBlank(job.variables.agentSlaJobType);
|
|
74
|
+
const slaQuestion = slaJobType
|
|
75
|
+
? await agentSlaEscalationQuestion(app.data, {
|
|
76
|
+
processInstanceKey: job.processInstanceKey != null ? String(job.processInstanceKey) : "",
|
|
77
|
+
jobType: slaJobType,
|
|
78
|
+
sla: nonBlank(job.variables.agentSlaTimeout) ?? "PT2H",
|
|
79
|
+
recovery: "Reply to retry, or investigate why no capacity was available.",
|
|
80
|
+
})
|
|
81
|
+
: undefined;
|
|
63
82
|
// Classify this job through the single canonical taxonomy (ADR 0002 §1). Only a
|
|
64
83
|
// decision-required escalation opens an answerable escalation row. A blank question (or any
|
|
65
84
|
// non-human-blocking status that reached here defensively) is a NON-escalation: open nothing
|
|
66
85
|
// and report `escalated:false` — never FABRICATE a question and never throw. The `gw-status`
|
|
67
86
|
// gateway already routes blank-question rounds back into the durable review wait, so this is
|
|
68
87
|
// defence-in-depth, not the primary guard.
|
|
69
|
-
const question = nonBlank(job.variables.question);
|
|
88
|
+
const question = slaQuestion ?? nonBlank(job.variables.question);
|
|
70
89
|
const disposition = classifyEscalation({ kind: "review-round", status: rawStatus, question });
|
|
71
90
|
if (disposition !== "decision-required" || question === undefined) {
|
|
72
|
-
return { escalationId: null, escalated: false };
|
|
91
|
+
return { escalationId: null, escalated: false, agentSlaJobType: null };
|
|
73
92
|
}
|
|
74
93
|
const kind = status === "needs_input" ? "question" : "blocker";
|
|
75
94
|
const now = new Date().toISOString();
|
|
@@ -142,7 +161,7 @@ const handler: AppJobHandler<In> = async (job, app) => {
|
|
|
142
161
|
});
|
|
143
162
|
});
|
|
144
163
|
|
|
145
|
-
return { escalationId: Number(escalationId), escalated: true, question };
|
|
164
|
+
return { escalationId: Number(escalationId), escalated: true, question, agentSlaJobType: null };
|
|
146
165
|
};
|
|
147
166
|
|
|
148
167
|
export default handler;
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
// variables through the user-task query, so the process variable must be persisted while it is still in
|
|
24
24
|
// scope on this job.
|
|
25
25
|
import type { AppJobHandler } from "@nanobpm/urban";
|
|
26
|
+
import { agentSlaEscalationQuestion } from "../../app/agentSlaEscalation.ts";
|
|
26
27
|
import { classifyEscalation } from "../../app/escalationTaxonomy.ts";
|
|
27
28
|
import { featureRuns, recordFeatureEscalation } from "../../app/feature.ts";
|
|
28
29
|
import { implementEscalationQuestion, NO_RESULT_QUESTION } from "../../app/implementEscalationReason.ts";
|
|
@@ -59,6 +60,13 @@ const handler: AppJobHandler<In, Out> = async (job, app) => {
|
|
|
59
60
|
// blank-question case.
|
|
60
61
|
const agentEscalated = classifyEscalation({ kind: "task", status: job.variables.status, question: rawQuestion }) ===
|
|
61
62
|
"decision-required";
|
|
63
|
+
// Agent-task SLA path (issue #879): the interrupting SLA timer boundary (not the agent) routed here,
|
|
64
|
+
// so there is NO agent result to classify — the job either never started (queue starvation) or ran but
|
|
65
|
+
// overran its budget. `agentSlaJobType` is set ONLY on the SLA-boundary arm (`record-escalation-sla`),
|
|
66
|
+
// so its presence authoritatively selects the SLA builder, which reads the durable activation ledger
|
|
67
|
+
// to distinguish never-started from started-but-hung and names the job type + how long it waited —
|
|
68
|
+
// never the old blanket "hung or looping" misdiagnosis of a queue-starved job.
|
|
69
|
+
const slaJobType = str(job.variables.agentSlaJobType);
|
|
62
70
|
// Build the escalation question through the single canonical builder (issue #865), ALWAYS — including
|
|
63
71
|
// when the agent raised its own answerable question. Passing the agent's `question` lets the builder
|
|
64
72
|
// lead with it AND fold in the agent's `summary` + transcript as supporting context (#865 review —
|
|
@@ -68,18 +76,26 @@ const handler: AppJobHandler<In, Out> = async (job, app) => {
|
|
|
68
76
|
// such as `completed`) and from a reported non-completion status — never the false "no status was
|
|
69
77
|
// reported" diagnosis for a reported `escalated`/`failed` (#865 review). The reason can never drift
|
|
70
78
|
// from the reconcile step because both read the same canonical builder.
|
|
71
|
-
const question =
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
const question = slaJobType
|
|
80
|
+
? await agentSlaEscalationQuestion(app.data, {
|
|
81
|
+
processInstanceKey: job.processInstanceKey != null ? String(job.processInstanceKey) : "",
|
|
82
|
+
jobType: slaJobType,
|
|
83
|
+
sla: str(job.variables.agentSlaTimeout) ?? "PT2H",
|
|
84
|
+
recovery:
|
|
85
|
+
"Answer to resume (the run continues on its deterministic feat/<task.id> branch), or abandon it.",
|
|
86
|
+
})
|
|
87
|
+
: implementEscalationQuestion({
|
|
88
|
+
status: job.variables.status,
|
|
89
|
+
question: rawQuestion,
|
|
90
|
+
summary: job.variables.summary,
|
|
91
|
+
transcriptUrl: job.variables.transcriptUrl,
|
|
92
|
+
// Delivery EVIDENCE (#865 review): the preserved PR (an `escalated`/`failed` with a blank
|
|
93
|
+
// question retains it — never claim "opened no pull request") and whether the reconcile step's
|
|
94
|
+
// GitHub lookup actually CONFIRMED the delivery state (only then may the reason say "none was
|
|
95
|
+
// found"). Both are process variables the upstream `reconcile-implement` step emits into scope.
|
|
96
|
+
pr: job.variables.pr,
|
|
97
|
+
deliveryVerified: job.variables.deliveryVerified,
|
|
98
|
+
});
|
|
83
99
|
|
|
84
100
|
// Append to the canonical `feature_escalations` audit log (the surviving table `pollUserTasks` reads),
|
|
85
101
|
// keyed by `subjectKey` — the feature run's `feature_key`, or the epic's `plan_key` for a wave slice.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Only `suite-failed` escalates. `clean` and textual `merge-conflict` proceed; D2/D6 own textual
|
|
3
3
|
// conflict ordering, while D3 owns clean-merge/combined-suite-red semantic conflicts.
|
|
4
4
|
import type { AppJobHandler } from "@nanobpm/urban";
|
|
5
|
+
import { agentSlaEscalationQuestion } from "../../app/agentSlaEscalation.ts";
|
|
5
6
|
import {
|
|
6
7
|
recordTrialMergeAudit,
|
|
7
8
|
type TrialMergeResult,
|
|
@@ -46,7 +47,21 @@ const handler: AppJobHandler<In, Out> = async (job, app) => {
|
|
|
46
47
|
const planKey = job.variables.planKey;
|
|
47
48
|
const wave = waveNo(job.variables.trialMergeWave ?? job.variables.currentWave);
|
|
48
49
|
const result = parseResult(job.variables.result);
|
|
49
|
-
|
|
50
|
+
// Agent-task SLA path (issue #879): the interrupting SLA timer boundary routed here with
|
|
51
|
+
// `agentSlaJobType` set, so there is no agent result — build the accurate never-started-vs-hung
|
|
52
|
+
// summary (named job type + wait duration) from the durable activation ledger instead of the old
|
|
53
|
+
// blanket "hung or looping" wording. On every other caller (a real agent result, the plan-fanout D3
|
|
54
|
+
// gate) `agentSlaJobType` is unset and the summary is derived from the agent's own `summary`/result.
|
|
55
|
+
const slaJobType = str(job.variables.agentSlaJobType);
|
|
56
|
+
const slaSummary = slaJobType
|
|
57
|
+
? await agentSlaEscalationQuestion(app.data, {
|
|
58
|
+
processInstanceKey: job.processInstanceKey != null ? String(job.processInstanceKey) : "",
|
|
59
|
+
jobType: slaJobType,
|
|
60
|
+
sla: str(job.variables.agentSlaTimeout) ?? "PT2H",
|
|
61
|
+
recovery: "Acknowledge to record the timeout, then decide whether to rerun the trial merge.",
|
|
62
|
+
})
|
|
63
|
+
: undefined;
|
|
64
|
+
const summary = slaSummary ?? str(job.variables.summary) ??
|
|
50
65
|
(result === "suite-failed" ? "Trial merge suite failed or returned no machine-readable result" : result);
|
|
51
66
|
const legacyJobKey = Reflect.get(job, "key");
|
|
52
67
|
const jobKey = job.jobKey ?? (legacyJobKey == null ? null : String(legacyJobKey));
|