@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 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"), "job:8420");
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"), "job:8419");
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 { jobStream } from "./correlation.ts";
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 (`job:<jobKey>`). A worker runs one job at a time in this fleet, but sorting keeps
142
- * it stable if it ever holds several. Undefined when the worker holds no claim — the caller then
143
- * falls back to the instance-keyed stream. This is the "relay demoted to drill-in, keyed by the
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 : jobStream(first);
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 (`job:<jobKey>`). */
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 jobKey-scoped `job:<jobKey>` stream, and the supply endpoint
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 (`job:<jobKey>` for a job stream). */
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 { jobStream } from "./correlation.ts";
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: jobStream("job-1"),
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(jobStream("job-1"))?.instance, "worker-A");
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: jobStream("job-iter-1"),
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: jobStream("job-iter-2"),
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: jobStream("job-2"),
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: jobStream("job-3"), completedAt: "2026-08-23T02:00:00.000Z" };
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: jobStream("job-4"), completedAt: "2026-08-23T02:00:00.000Z" };
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: jobStream("job-5"), completedAt: "2026-08-23T03:00:00.000Z", instance: "worker-E" });
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: jobStream("job-5"), completedAt: "2026-08-23T03:05:00.000Z", instance: "worker-E" });
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 a `job:<jobKey>` stream id, or undefined for a non-job stream. */
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 = jobKeyOfStream(stream);
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 `job:<jobKey>` stream convention; the
5
- // two derived-from-one-write projections (instance→jobKeys and jobKey→context) staying consistent
6
- // across link / re-link (move) / releaseJob / releaseInstance; the presence `jobKeysFor` seam; the
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 / jobKeyOfStream are inverse over the job: convention", () => {
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(jobKeyOfStream(jobStream("6494")), "6494");
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("link records context and both projections; resolve carries the job: stream", () => {
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, "job:6494");
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, "job:6494");
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"), "job:10");
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), ["job:10", "job:20", "job:30"]);
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 jobKey-scoped stream {@link jobStream} — a stable naming
16
- // convention (`job:<jobKey>`) so the report can repoint a worker's drill stream at its live job
17
- // without a second lookup table.
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
- /** The relay-stream prefix for a jobKey-scoped terminal stream. */
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 stable relay stream id a worker relays a job's terminal on: `job:<jobKey>`. */
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-scoped relay stream id, or undefined for any other stream.
39
- * A bare `job:` prefix with no suffix carries no jobKey, so it maps to undefined too — keeping
40
- * the "empty jobKey is invalid" invariant (`link()` ignores empty jobKeys) consistent for callers.
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 jobKeyOfStream(stream: string): string | undefined {
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 (`job:<jobKey>`). */
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: jobStream(jobKey),
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 : jobStream(first);
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"), "job:8420", "the drill stream repoints at the claimed job");
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
  }