@nanobpm/nano-workforce 0.183.0 → 0.183.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 +12 -0
- package/app/agentic/claim-registry.test.ts +3 -2
- package/app/agentic/claim-registry.ts +8 -6
- package/app/agentic/cockpit/supply-view.ts +3 -3
- package/app/agentic/cockpit/transcript-view.ts +1 -1
- package/app/agentic/correlation-store.test.ts +14 -10
- package/app/agentic/correlation-store.ts +4 -3
- package/app/agentic/correlation.test.ts +24 -16
- package/app/agentic/correlation.ts +25 -12
- package/app/agentic/families/claim.family.test.ts +2 -1
- package/app/agentic/families/relay.family.test.ts +74 -73
- package/app/agentic/families/relay.family.ts +24 -21
- package/app/agentic/transcript-read.test.ts +108 -17
- package/app/agentic/transcript-read.ts +41 -7
- package/app/contracts.ts +1 -1
- package/app/reconcile.test.ts +283 -19
- package/app/reconcile.ts +200 -23
- package/operations/getAgenticSupply.test.ts +41 -2
- package/operations/getAgenticSupply.ts +7 -5
- package/operations/getAgenticTranscript.test.ts +5 -4
- package/operations/listAgenticTranscripts.test.ts +10 -9
- package/package.json +1 -1
- package/test/agentic-e2e.test.ts +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [0.183.2](https://github.com/nanobpm/nano-workforce/compare/v0.183.1...v0.183.2) (2026-09-07)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
* **cockpit:** read transcripts on instance-scoped composeStreamId stream, not job:<jobKey> ([#754](https://github.com/nanobpm/nano-workforce/issues/754)) ([03fabb5](https://github.com/nanobpm/nano-workforce/commit/03fabb57c79c25ac0334bc0bd4a672348e5e507c)), closes [c8ctl-plugin-nano#186](https://github.com/nanobpm/c8ctl-plugin-nano/issues/186) [nano-ide#557](https://github.com/nanobpm/nano-ide/issues/557) [#543](https://github.com/nanobpm/nano-workforce/issues/543) [#738](https://github.com/nanobpm/nano-workforce/issues/738) [#738](https://github.com/nanobpm/nano-workforce/issues/738) [#738](https://github.com/nanobpm/nano-workforce/issues/738) [#543](https://github.com/nanobpm/nano-workforce/issues/543)
|
|
6
|
+
|
|
7
|
+
## [0.183.1](https://github.com/nanobpm/nano-workforce/compare/v0.183.0...v0.183.1) (2026-09-07)
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* **reconcile:** cross-check engine truth before orphaning a vanished instance ([#753](https://github.com/nanobpm/nano-workforce/issues/753)) ([a363fdb](https://github.com/nanobpm/nano-workforce/commit/a363fdbec1fd14641f2c4a2ef2718034a11a1e92)), closes [#630](https://github.com/nanobpm/nano-workforce/issues/630) [#736](https://github.com/nanobpm/nano-workforce/issues/736) [#736](https://github.com/nanobpm/nano-workforce/issues/736) [#736](https://github.com/nanobpm/nano-workforce/issues/736)
|
|
12
|
+
|
|
1
13
|
## [0.183.0](https://github.com/nanobpm/nano-workforce/compare/v0.182.3...v0.183.0) (2026-09-07)
|
|
2
14
|
|
|
3
15
|
### Features
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import assert from "node:assert/strict";
|
|
10
10
|
import { test } from "node:test";
|
|
11
11
|
|
|
12
|
+
import { composeStreamId } from "@nanobpm/agentic/emit";
|
|
12
13
|
import {
|
|
13
14
|
ClaimRegistry,
|
|
14
15
|
currentClaimRegistry,
|
|
@@ -22,7 +23,7 @@ test("claim records both projections; jobKeysFor and primaryStreamFor resolve ag
|
|
|
22
23
|
assert.deepEqual(reg.jobKeysFor("wk-a"), ["8420"]);
|
|
23
24
|
assert.equal(reg.ownerOf("8420"), "wk-a");
|
|
24
25
|
assert.equal(reg.isClaimed("8420"), true);
|
|
25
|
-
assert.equal(reg.primaryStreamFor("wk-a"), "
|
|
26
|
+
assert.equal(reg.primaryStreamFor("wk-a"), composeStreamId("wk-a", "8420"));
|
|
26
27
|
assert.equal(reg.count(), 1);
|
|
27
28
|
});
|
|
28
29
|
|
|
@@ -62,7 +63,7 @@ test("a worker can hold several claims; jobKeysFor is sorted and primaryStreamFo
|
|
|
62
63
|
reg.claim("wk-a", "8419");
|
|
63
64
|
reg.claim("wk-a", "8421");
|
|
64
65
|
assert.deepEqual(reg.jobKeysFor("wk-a"), ["8419", "8420", "8421"]);
|
|
65
|
-
assert.equal(reg.primaryStreamFor("wk-a"), "
|
|
66
|
+
assert.equal(reg.primaryStreamFor("wk-a"), composeStreamId("wk-a", "8419"));
|
|
66
67
|
});
|
|
67
68
|
|
|
68
69
|
test("release clears one claim; a late / duplicate release is a no-op", () => {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
// type on the job protocol; ADVISORY — the registry is a read-only visibility source and NEVER
|
|
26
26
|
// hard-locks or gates a BPMN sequence flow.
|
|
27
27
|
|
|
28
|
-
import {
|
|
28
|
+
import { composeStreamId } from "@nanobpm/agentic/emit";
|
|
29
29
|
|
|
30
30
|
/** One ownership record: a worker instance's claim over a job. */
|
|
31
31
|
export interface Claim {
|
|
@@ -138,14 +138,16 @@ export class ClaimRegistry {
|
|
|
138
138
|
|
|
139
139
|
/**
|
|
140
140
|
* The jobKey-scoped relay stream a worker's terminal should drill into: its lowest-sorted current
|
|
141
|
-
* claim's stream (`
|
|
142
|
-
* it stable if it ever holds several. Undefined when the worker holds no
|
|
143
|
-
* falls back to the instance-keyed stream. This is the "relay demoted to
|
|
144
|
-
* CLAIM (not by `instanceForConnection`)" seam.
|
|
141
|
+
* claim's stream (`composeStreamId(instance, jobKey)`). A worker runs one job at a time in this
|
|
142
|
+
* fleet, but sorting keeps it stable if it ever holds several. Undefined when the worker holds no
|
|
143
|
+
* claim — the caller then falls back to the instance-keyed stream. This is the "relay demoted to
|
|
144
|
+
* drill-in, keyed by the CLAIM (not by `instanceForConnection`)" seam. The instance-scoped stream id
|
|
145
|
+
* is the one canonical `@nanobpm/agentic/emit` codec the producer writes under (issue #738), so the
|
|
146
|
+
* cockpit drills the SAME stream the transcript bytes actually land on.
|
|
145
147
|
*/
|
|
146
148
|
primaryStreamFor(instance: string): string | undefined {
|
|
147
149
|
const [first] = this.jobKeysFor(instance);
|
|
148
|
-
return first === undefined ? undefined :
|
|
150
|
+
return first === undefined ? undefined : composeStreamId(instance, first);
|
|
149
151
|
}
|
|
150
152
|
|
|
151
153
|
/** Whether any instance currently owns `jobKey`. */
|
|
@@ -53,7 +53,7 @@ export interface SupplyLeafReport {
|
|
|
53
53
|
export interface SupplyCorrelationReport {
|
|
54
54
|
/** The Camunda-8 job key. */
|
|
55
55
|
readonly jobKey: string;
|
|
56
|
-
/** The relay stream the job's terminal is on (`
|
|
56
|
+
/** The relay stream the job's terminal is on (`composeStreamId(instance, jobKey)`, issue #738). */
|
|
57
57
|
readonly stream: string;
|
|
58
58
|
/** The owning process instance key, if known. */
|
|
59
59
|
readonly processInstanceKey?: string;
|
|
@@ -113,8 +113,8 @@ export interface SupplyWorkerView {
|
|
|
113
113
|
readonly jobs: number;
|
|
114
114
|
/**
|
|
115
115
|
* Whether this worker has a LIVE terminal to drill into. True only while it holds a current job:
|
|
116
|
-
* a worker relays its terminal on the
|
|
117
|
-
* repoints {@link stream} at it. An IDLE worker (no jobs) has its `stream` default back to the bare
|
|
116
|
+
* a worker relays its terminal on the instance-scoped `composeStreamId(instance, jobKey)` stream
|
|
117
|
+
* (issue #738), and the supply endpoint repoints {@link stream} at it. An IDLE worker (no jobs) has its `stream` default back to the bare
|
|
118
118
|
* instance id — a stream NO producer ever writes to — so drilling it opens a permanently blank
|
|
119
119
|
* "live" terminal. The renderer suppresses the drill affordance when this is false.
|
|
120
120
|
*/
|
|
@@ -41,7 +41,7 @@ export interface TranscriptListReport {
|
|
|
41
41
|
|
|
42
42
|
/** One past-session row in the renderable history view. */
|
|
43
43
|
export interface TranscriptView {
|
|
44
|
-
/** The relay stream id to replay (`
|
|
44
|
+
/** The relay stream id to replay (`composeStreamId(instance, jobKey)` for a job stream, issue #738). */
|
|
45
45
|
readonly stream: string;
|
|
46
46
|
/** A single stable human label for the session's process instance / plan (falls back to the stream). */
|
|
47
47
|
readonly label: string;
|
|
@@ -10,9 +10,13 @@ import { test } from "node:test";
|
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
11
|
import type { SqliteDb } from "@nanobpm/agentic/transcript";
|
|
12
12
|
import { assert, assertEquals } from "#test-assert";
|
|
13
|
-
import {
|
|
13
|
+
import { composeStreamId } from "@nanobpm/agentic/emit";
|
|
14
14
|
import { AGENTIC_CORRELATION_SCHEMA_SQL, AgenticCorrelationStore } from "./correlation-store.ts";
|
|
15
15
|
|
|
16
|
+
/** The instance-scoped transcript stream id a job's terminal is stored under (issue #738). The
|
|
17
|
+
* worker instance is fixed here — `byStream` only recovers the jobKey (the stream part) from it. */
|
|
18
|
+
const st = (jobKey: string): string => composeStreamId("worker-A", jobKey);
|
|
19
|
+
|
|
16
20
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
17
21
|
|
|
18
22
|
function memoryDb(): SqliteDb {
|
|
@@ -85,7 +89,7 @@ test("record + get round-trips full attribution, and byStream decodes the jobKey
|
|
|
85
89
|
const store = new AgenticCorrelationStore(memoryDb());
|
|
86
90
|
store.record({
|
|
87
91
|
jobKey: "job-1",
|
|
88
|
-
stream:
|
|
92
|
+
stream: st("job-1"),
|
|
89
93
|
instance: "worker-A",
|
|
90
94
|
identity: "leaf:token",
|
|
91
95
|
host: "merlin.local",
|
|
@@ -105,7 +109,7 @@ test("record + get round-trips full attribution, and byStream decodes the jobKey
|
|
|
105
109
|
assertEquals(got?.planKey, "owner/repo#142");
|
|
106
110
|
assertEquals(got?.completedAt, "2026-08-23T00:05:00.000Z");
|
|
107
111
|
// The same row is reachable from the stream id.
|
|
108
|
-
assertEquals(store.byStream(
|
|
112
|
+
assertEquals(store.byStream(st("job-1"))?.instance, "worker-A");
|
|
109
113
|
});
|
|
110
114
|
|
|
111
115
|
test("byElementInstance keys per-occupancy, distinguishing a looping/retried activity's iterations", () => {
|
|
@@ -115,7 +119,7 @@ test("byElementInstance keys per-occupancy, distinguishing a looping/retried act
|
|
|
115
119
|
// element-instance key each resolves to its own attribution (the whole point of #544).
|
|
116
120
|
store.record({
|
|
117
121
|
jobKey: "job-iter-1",
|
|
118
|
-
stream:
|
|
122
|
+
stream: st("job-iter-1"),
|
|
119
123
|
instance: "worker-A",
|
|
120
124
|
processInstanceKey: "pi-9",
|
|
121
125
|
elementId: "agent",
|
|
@@ -124,7 +128,7 @@ test("byElementInstance keys per-occupancy, distinguishing a looping/retried act
|
|
|
124
128
|
});
|
|
125
129
|
store.record({
|
|
126
130
|
jobKey: "job-iter-2",
|
|
127
|
-
stream:
|
|
131
|
+
stream: st("job-iter-2"),
|
|
128
132
|
instance: "worker-A",
|
|
129
133
|
processInstanceKey: "pi-9",
|
|
130
134
|
elementId: "agent",
|
|
@@ -147,7 +151,7 @@ test("optional context columns are omitted (not null) when unknown", () => {
|
|
|
147
151
|
const store = new AgenticCorrelationStore(memoryDb());
|
|
148
152
|
store.record({
|
|
149
153
|
jobKey: "job-2",
|
|
150
|
-
stream:
|
|
154
|
+
stream: st("job-2"),
|
|
151
155
|
instance: "worker-B",
|
|
152
156
|
completedAt: "2026-08-23T01:00:00.000Z",
|
|
153
157
|
});
|
|
@@ -159,7 +163,7 @@ test("optional context columns are omitted (not null) when unknown", () => {
|
|
|
159
163
|
|
|
160
164
|
test("record is an upsert: re-recording a jobKey is last-write-wins", () => {
|
|
161
165
|
const store = new AgenticCorrelationStore(memoryDb());
|
|
162
|
-
const base = { jobKey: "job-3", stream:
|
|
166
|
+
const base = { jobKey: "job-3", stream: st("job-3"), completedAt: "2026-08-23T02:00:00.000Z" };
|
|
163
167
|
store.record({ ...base, instance: "worker-C" });
|
|
164
168
|
store.record({ ...base, instance: "worker-C", host: "second.local", completedAt: "2026-08-23T02:10:00.000Z" });
|
|
165
169
|
const got = store.get("job-3");
|
|
@@ -169,7 +173,7 @@ test("record is an upsert: re-recording a jobKey is last-write-wins", () => {
|
|
|
169
173
|
|
|
170
174
|
test("record preserves an existing element_instance_key when a later re-record omits it (monotonic)", () => {
|
|
171
175
|
const store = new AgenticCorrelationStore(memoryDb());
|
|
172
|
-
const base = { jobKey: "job-4", stream:
|
|
176
|
+
const base = { jobKey: "job-4", stream: st("job-4"), completedAt: "2026-08-23T02:00:00.000Z" };
|
|
173
177
|
// The durable backfill path (or a first record that carried the resolved key).
|
|
174
178
|
store.record({ ...base, instance: "worker-D", elementInstanceKey: "ei-777" });
|
|
175
179
|
// A later best-effort re-record that does NOT know the key must not wipe it back to NULL.
|
|
@@ -178,9 +182,9 @@ test("record preserves an existing element_instance_key when a later re-record o
|
|
|
178
182
|
assertEquals(got?.host, "later.local");
|
|
179
183
|
assertEquals(got?.elementInstanceKey, "ei-777");
|
|
180
184
|
// setElementInstanceKey backfill then a bare re-record likewise survives.
|
|
181
|
-
store.record({ jobKey: "job-5", stream:
|
|
185
|
+
store.record({ jobKey: "job-5", stream: st("job-5"), completedAt: "2026-08-23T03:00:00.000Z", instance: "worker-E" });
|
|
182
186
|
store.setElementInstanceKey("job-5", "ei-888");
|
|
183
|
-
store.record({ jobKey: "job-5", stream:
|
|
187
|
+
store.record({ jobKey: "job-5", stream: st("job-5"), completedAt: "2026-08-23T03:05:00.000Z", instance: "worker-E" });
|
|
184
188
|
assertEquals(store.get("job-5")?.elementInstanceKey, "ei-888");
|
|
185
189
|
});
|
|
186
190
|
|
|
@@ -19,8 +19,9 @@
|
|
|
19
19
|
// `element_instance_key`). A drift-guard test (`correlation-store.test.ts`) applies those migrations to
|
|
20
20
|
// one DB and the canonical DDL to another and asserts the two schemas are identical, so they can never
|
|
21
21
|
// diverge (the migrations, once merged, are immutable — the canonical DDL is what evolves).
|
|
22
|
+
|
|
23
|
+
import { parseStreamId } from "@nanobpm/agentic/emit";
|
|
22
24
|
import type { SqliteDb } from "@nanobpm/agentic/transcript";
|
|
23
|
-
import { jobKeyOfStream } from "./correlation.ts";
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* The canonical DDL for the durable correlation table. The `078_*` + `086_*` migrations reproduce this
|
|
@@ -183,9 +184,9 @@ export class AgenticCorrelationStore {
|
|
|
183
184
|
return rows.length > 0 ? fromRow(rows[0]) : undefined;
|
|
184
185
|
}
|
|
185
186
|
|
|
186
|
-
/** The durable attribution for
|
|
187
|
+
/** The durable attribution for an instance-scoped job stream id, or undefined for a non-job stream. */
|
|
187
188
|
byStream(stream: string): DurableCorrelation | undefined {
|
|
188
|
-
const jobKey =
|
|
189
|
+
const jobKey = parseStreamId(stream)?.stream;
|
|
189
190
|
return jobKey === undefined ? undefined : this.get(jobKey);
|
|
190
191
|
}
|
|
191
192
|
|
|
@@ -1,32 +1,36 @@
|
|
|
1
1
|
// Unit tests for the jobKey ⇄ process/plan correlation registry (ADR 0056, H6 / #149).
|
|
2
2
|
//
|
|
3
3
|
// The registry is the single canonical join the cockpit uses to line a worker's terminal up with the
|
|
4
|
-
// process instance / plan it belongs to. These tests pin: the `
|
|
5
|
-
// two derived-from-one-write projections
|
|
6
|
-
// across link / re-link (move) / releaseJob /
|
|
7
|
-
// drill `primaryStreamFor`; and the sorted snapshot.
|
|
4
|
+
// process instance / plan it belongs to. These tests pin: the instance-scoped `composeStreamId`
|
|
5
|
+
// transcript-stream convention (issue #738); the two derived-from-one-write projections
|
|
6
|
+
// (instance→jobKeys and jobKey→context) staying consistent across link / re-link (move) / releaseJob /
|
|
7
|
+
// releaseInstance; the presence `jobKeysFor` seam; the drill `primaryStreamFor`; and the sorted snapshot.
|
|
8
8
|
import assert from "node:assert/strict";
|
|
9
9
|
import { test } from "node:test";
|
|
10
10
|
|
|
11
|
+
import { composeStreamId, parseStreamId } from "@nanobpm/agentic/emit";
|
|
11
12
|
import {
|
|
12
13
|
CorrelationRegistry,
|
|
13
14
|
currentCorrelation,
|
|
14
15
|
JOB_STREAM_PREFIX,
|
|
15
|
-
jobKeyOfStream,
|
|
16
16
|
jobStream,
|
|
17
17
|
setCurrentCorrelation,
|
|
18
18
|
} from "./correlation.ts";
|
|
19
19
|
|
|
20
|
-
test("jobStream
|
|
20
|
+
test("jobStream builds the bare, slash-free job: id retained for the aux (Explorer/permission) surfaces", () => {
|
|
21
21
|
assert.equal(jobStream("6494"), `${JOB_STREAM_PREFIX}6494`);
|
|
22
|
-
assert.equal(
|
|
23
|
-
assert.equal(jobKeyOfStream("wk-a"), undefined);
|
|
24
|
-
// A bare `job:` prefix carries no jobKey, so it maps to undefined (not "") — an empty jobKey is
|
|
25
|
-
// invalid (link() ignores it), so callers never mistake it for a valid key.
|
|
26
|
-
assert.equal(jobKeyOfStream("job:"), undefined);
|
|
22
|
+
assert.equal(JOB_STREAM_PREFIX, "job:");
|
|
27
23
|
});
|
|
28
24
|
|
|
29
|
-
test("
|
|
25
|
+
test("the transcript stream is the instance-scoped composeStreamId id, round-tripped by parseStreamId (#738)", () => {
|
|
26
|
+
const stream = composeStreamId("wk-a", "6494");
|
|
27
|
+
const ref = parseStreamId(stream);
|
|
28
|
+
assert.ok(ref);
|
|
29
|
+
assert.equal(ref.instance, "wk-a");
|
|
30
|
+
assert.equal(ref.stream, "6494");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("link records context and both projections; resolve carries the instance-scoped stream", () => {
|
|
30
34
|
const reg = new CorrelationRegistry();
|
|
31
35
|
reg.link("wk-a", "6494", { processInstanceKey: "4612", bpmnProcessId: "plan-fanout", elementId: "implement-task", planKey: "o/r#142" });
|
|
32
36
|
|
|
@@ -34,7 +38,7 @@ test("link records context and both projections; resolve carries the job: stream
|
|
|
34
38
|
const c = reg.resolve("6494");
|
|
35
39
|
assert.ok(c);
|
|
36
40
|
assert.equal(c.jobKey, "6494");
|
|
37
|
-
assert.equal(c.stream, "
|
|
41
|
+
assert.equal(c.stream, composeStreamId("wk-a", "6494"));
|
|
38
42
|
assert.equal(c.processInstanceKey, "4612");
|
|
39
43
|
assert.equal(c.bpmnProcessId, "plan-fanout");
|
|
40
44
|
assert.equal(c.elementId, "implement-task");
|
|
@@ -54,7 +58,7 @@ test("attachElementInstance enriches a linked job's context, preserving every ot
|
|
|
54
58
|
assert.equal(c.elementId, "agent");
|
|
55
59
|
assert.equal(c.planKey, "o/r#142");
|
|
56
60
|
assert.equal(c.jobKey, "6494");
|
|
57
|
-
assert.equal(c.stream, "
|
|
61
|
+
assert.equal(c.stream, composeStreamId("wk-a", "6494"));
|
|
58
62
|
});
|
|
59
63
|
|
|
60
64
|
test("attachElementInstance is a no-op for a released (or never-linked) job or an empty key (#544)", () => {
|
|
@@ -145,7 +149,7 @@ test("primaryStreamFor picks the lowest-sorted job's stream; undefined when none
|
|
|
145
149
|
assert.equal(reg.primaryStreamFor("wk-a"), undefined);
|
|
146
150
|
reg.link("wk-a", "50");
|
|
147
151
|
reg.link("wk-a", "10");
|
|
148
|
-
assert.equal(reg.primaryStreamFor("wk-a"), "
|
|
152
|
+
assert.equal(reg.primaryStreamFor("wk-a"), composeStreamId("wk-a", "10"));
|
|
149
153
|
});
|
|
150
154
|
|
|
151
155
|
test("snapshot returns every job sorted by jobKey", () => {
|
|
@@ -156,7 +160,11 @@ test("snapshot returns every job sorted by jobKey", () => {
|
|
|
156
160
|
const snap = reg.snapshot();
|
|
157
161
|
assert.equal(snap.count, 3);
|
|
158
162
|
assert.deepEqual(snap.correlations.map((c) => c.jobKey), ["10", "20", "30"]);
|
|
159
|
-
assert.deepEqual(snap.correlations.map((c) => c.stream), [
|
|
163
|
+
assert.deepEqual(snap.correlations.map((c) => c.stream), [
|
|
164
|
+
composeStreamId("wk-b", "10"),
|
|
165
|
+
composeStreamId("wk-c", "20"),
|
|
166
|
+
composeStreamId("wk-a", "30"),
|
|
167
|
+
]);
|
|
160
168
|
});
|
|
161
169
|
|
|
162
170
|
test("link with no context leaves optional fields unset (no undefined holes)", () => {
|
|
@@ -12,9 +12,10 @@
|
|
|
12
12
|
// asks for, so a worker's current jobKeys light up in the supply feed / cockpit.
|
|
13
13
|
// - `resolve(jobKey)` gives the cockpit the process-instance / plan a terminal belongs to, so the
|
|
14
14
|
// drilled bytes line up with "that process instance / this plan".
|
|
15
|
-
// - the relay terminal for a job is the
|
|
16
|
-
//
|
|
17
|
-
//
|
|
15
|
+
// - the relay terminal for a job is the instance-scoped stream `composeStreamId(instance, jobKey)`
|
|
16
|
+
// from `@nanobpm/agentic/emit` (issue #738) — the ONE codec the producer also writes with, so the
|
|
17
|
+
// report can repoint a worker's drill stream at the exact live stream the producer emits without a
|
|
18
|
+
// second, divergent naming scheme.
|
|
18
19
|
//
|
|
19
20
|
// Who populates it: the orchestrator that dispatches an agentic job to a worker (it holds the whole
|
|
20
21
|
// job payload — jobKey, processInstanceKey, bpmnProcessId, and the plan/epic it belongs to) calls
|
|
@@ -26,20 +27,32 @@
|
|
|
26
27
|
// is untouched — correlation is an app-side observation, not a new wire type; ADVISORY — it is a
|
|
27
28
|
// read-only join for visibility and NEVER hard-locks or gates a BPMN sequence flow.
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
import { composeStreamId } from "@nanobpm/agentic/emit";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The relay-stream prefix for a jobKey-scoped terminal stream. Retained ONLY for the two surfaces
|
|
34
|
+
* that still address a job by a bare, slash-free stream id — the Explorer Stage-0 transcript URL
|
|
35
|
+
* (`app/agentic/transcript-url.ts`, #543) and the permission control-lane resolution default
|
|
36
|
+
* (`app/agentic/permission-bridge.ts`). The cockpit transcript DATA plane no longer uses it: a job
|
|
37
|
+
* transcript stream is the instance-scoped `composeStreamId(instance, jobKey)` (issue #738), so that
|
|
38
|
+
* producer and consumer share ONE codec (`@nanobpm/agentic/emit`) and never diverge again.
|
|
39
|
+
*/
|
|
30
40
|
export const JOB_STREAM_PREFIX = "job:";
|
|
31
41
|
|
|
32
|
-
/** The
|
|
42
|
+
/** The bare, slash-free jobKey-scoped stream id `job:<jobKey>` — see {@link JOB_STREAM_PREFIX}. */
|
|
33
43
|
export function jobStream(jobKey: string): string {
|
|
34
44
|
return `${JOB_STREAM_PREFIX}${jobKey}`;
|
|
35
45
|
}
|
|
36
46
|
|
|
37
47
|
/**
|
|
38
|
-
* The jobKey encoded in a jobKey
|
|
39
|
-
*
|
|
40
|
-
*
|
|
48
|
+
* The jobKey encoded in a bare `job:<jobKey>` alias — the Stage-0 transcript URL / permission-lane
|
|
49
|
+
* scheme retained by {@link JOB_STREAM_PREFIX}. Returns undefined for ANY other stream, including an
|
|
50
|
+
* instance-scoped `composeStreamId(instance, jobKey)` data-plane id (decode THAT with `parseStreamId`)
|
|
51
|
+
* and a bare `job:` with no jobKey — keeping the "empty jobKey is invalid" invariant (`link()` ignores
|
|
52
|
+
* empty jobKeys) consistent for callers. This is the READ-path alias decoder ONLY: the write/relay side
|
|
53
|
+
* addresses jobs solely by the instance-scoped id, so it must never route through this.
|
|
41
54
|
*/
|
|
42
|
-
export function
|
|
55
|
+
export function jobKeyOfJobStream(stream: string): string | undefined {
|
|
43
56
|
if (!stream.startsWith(JOB_STREAM_PREFIX)) return undefined;
|
|
44
57
|
const jobKey = stream.slice(JOB_STREAM_PREFIX.length);
|
|
45
58
|
return jobKey === "" ? undefined : jobKey;
|
|
@@ -64,7 +77,7 @@ export interface JobCorrelation {
|
|
|
64
77
|
readonly elementInstanceKey?: string;
|
|
65
78
|
/** The plan / epic key this job is part of (e.g. `owner/repo#142`), if known. */
|
|
66
79
|
readonly planKey?: string;
|
|
67
|
-
/** The relay stream id the job's terminal is relayed on (`
|
|
80
|
+
/** The relay stream id the job's terminal is relayed on (`composeStreamId(instance, jobKey)`). */
|
|
68
81
|
readonly stream: string;
|
|
69
82
|
}
|
|
70
83
|
|
|
@@ -120,7 +133,7 @@ export class CorrelationRegistry {
|
|
|
120
133
|
this.#context.set(jobKey, {
|
|
121
134
|
...existing,
|
|
122
135
|
jobKey,
|
|
123
|
-
stream:
|
|
136
|
+
stream: composeStreamId(instance, jobKey),
|
|
124
137
|
...stripUndefined(context),
|
|
125
138
|
});
|
|
126
139
|
}
|
|
@@ -183,7 +196,7 @@ export class CorrelationRegistry {
|
|
|
183
196
|
*/
|
|
184
197
|
primaryStreamFor(instance: string): string | undefined {
|
|
185
198
|
const [first] = this.jobKeysFor(instance);
|
|
186
|
-
return first === undefined ? undefined :
|
|
199
|
+
return first === undefined ? undefined : composeStreamId(instance, first);
|
|
187
200
|
}
|
|
188
201
|
|
|
189
202
|
/** The number of currently-linked jobs. */
|
|
@@ -12,6 +12,7 @@ import { AgenticHub } from "@nanobpm/agentic/channel";
|
|
|
12
12
|
import type { Authenticator, ChannelConnection, ChannelTransport } from "@nanobpm/agentic/channel";
|
|
13
13
|
import { encodeFrame, type Frame } from "@nanobpm/agentic/protocol";
|
|
14
14
|
import { assert, assertEquals } from "#test-assert";
|
|
15
|
+
import { composeStreamId } from "@nanobpm/agentic/emit";
|
|
15
16
|
import { noopLog } from "../../../test/log.ts";
|
|
16
17
|
import { currentClaimRegistry } from "../claim-registry.ts";
|
|
17
18
|
import type { AgenticContext } from "../registry.ts";
|
|
@@ -91,7 +92,7 @@ test("a claim populates the worker's jobKeys with ZERO transcript (visibility no
|
|
|
91
92
|
const reg = currentClaimRegistry();
|
|
92
93
|
assert(reg, "registry mounted");
|
|
93
94
|
assertEquals(reg.jobKeysFor("wk-a"), ["8420"]);
|
|
94
|
-
assertEquals(reg.primaryStreamFor("wk-a"), "
|
|
95
|
+
assertEquals(reg.primaryStreamFor("wk-a"), composeStreamId("wk-a", "8420"), "the drill stream repoints at the claimed job");
|
|
95
96
|
} finally {
|
|
96
97
|
family.teardown?.();
|
|
97
98
|
}
|