@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.
@@ -12,6 +12,7 @@ import { encodeFrame, type Frame } from "@nanobpm/agentic/protocol";
12
12
  import type { SqliteDb } from "@nanobpm/agentic/presence";
13
13
  import type { AppApi, DataLayer } from "@nanobpm/urban";
14
14
  import { assert, assertEquals } from "#test-assert";
15
+ import { composeStreamId, parseStreamId } from "@nanobpm/agentic/emit";
15
16
  import { currentClaimRegistry } from "../app/agentic/claim-registry.ts";
16
17
  import { currentCorrelation } from "../app/agentic/correlation.ts";
17
18
  import { family as claimFamily } from "../app/agentic/families/claim.family.ts";
@@ -172,7 +173,7 @@ test("#713: a claim populates jobKeys and repoints the drill stream with ZERO tr
172
173
  assertEquals(res.status, 200);
173
174
  const w = res.body.workers[0];
174
175
  assertEquals(w.jobKeys, ["8420"], "the claim registry feeds the jobKeys seam");
175
- assertEquals(w.stream, "job:8420", "the drill stream repoints at the claimed job, keyed by the claim");
176
+ assertEquals(w.stream, composeStreamId("wk-a", "8420"), "the drill stream repoints at the claimed job, keyed by the claim");
176
177
  assertEquals(res.body.correlations.length, 0, "no correlation context until a terminal lands (drill-in only)");
177
178
  } finally {
178
179
  claimFamily.teardown?.();
@@ -210,7 +211,7 @@ test("#713: the correlation registry is demoted to drill-in context — a link a
210
211
  assertEquals(res.body.correlations.length, 1);
211
212
  const c = res.body.correlations[0];
212
213
  assertEquals(c.jobKey, "6494");
213
- assertEquals(c.stream, "job:6494");
214
+ assertEquals(c.stream, composeStreamId("wk-a", "6494"));
214
215
  assertEquals(c.processInstanceKey, "4612");
215
216
  assertEquals(c.bpmnProcessId, "plan-fanout");
216
217
  assertEquals(c.planKey, "o/r#142");
@@ -220,3 +221,41 @@ test("#713: the correlation registry is demoted to drill-in context — a link a
220
221
  await hub.close();
221
222
  }
222
223
  });
224
+
225
+ test("#738 drift: the supply advertises the producer's instance-scoped stream id, round-tripping the shared @nanobpm/agentic codec (not the retired job:<jobKey>)", async () => {
226
+ // The data-plane naming contract this bug (#738) restores: the producer (c8ctl-plugin-nano) writes a
227
+ // job's transcript under `composeStreamId(instance, jobKey)`, so the cockpit MUST advertise that exact
228
+ // id — both the worker drill stream (claim-keyed) and the correlation drill stream — or every
229
+ // transcript renders empty. This pins the consumer to the ONE shared codec: a regression back to
230
+ // `job:<jobKey>` (a slash-free id that never round-trips through parseStreamId) fails here.
231
+ const hub = await mountPresence(memSqlite());
232
+ const ctx: AgenticContext = { hub, registry: hub.registry, transport: undefined as never, data: undefined, log: noopLog() };
233
+ claimFamily.mount(ctx);
234
+ correlationFamily.mount(ctx);
235
+ const claims = currentClaimRegistry();
236
+ const correlation = currentCorrelation();
237
+ assert(claims !== undefined && correlation !== undefined, "both singletons install");
238
+ claims.claim("wk-a", "12559");
239
+ correlation.link("wk-a", "12559", { processInstanceKey: "4612", planKey: "o/r#142" });
240
+ try {
241
+ const res = (await handler(input(), app)) as {
242
+ status: number;
243
+ body: { workers: Array<Record<string, unknown>>; correlations: Array<Record<string, unknown>> };
244
+ };
245
+ assertEquals(res.status, 200);
246
+ const producerStream = composeStreamId("wk-a", "12559");
247
+ const w = res.body.workers[0];
248
+ assertEquals(w.stream, producerStream, "the cockpit drills the exact stream the producer writes");
249
+ const c = res.body.correlations[0];
250
+ assertEquals(c.stream, producerStream, "the correlation drill stream matches the producer stream");
251
+ // The shared codec round-trips the advertised id back to its {instance, stream} parts.
252
+ assertEquals(parseStreamId(producerStream), { instance: "wk-a", stream: "12559" });
253
+ // And it is emphatically NOT the retired job:<jobKey> scheme that left transcripts empty (#738).
254
+ assert(typeof w.stream === "string" && !w.stream.startsWith("job:"), "the retired job:<jobKey> data-plane scheme is gone");
255
+ } finally {
256
+ correlationFamily.teardown?.();
257
+ claimFamily.teardown?.();
258
+ family.teardown?.();
259
+ await hub.close();
260
+ }
261
+ });
@@ -6,8 +6,9 @@
6
6
  // H6/#713 closes the loop with an EXPLICIT claim registry (`app/agentic/claim-registry.ts`): it is the
7
7
  // AUTHORITATIVE source the presence snapshot's `jobKeysFor` seam resolves against, so each worker's
8
8
  // current jobKeys light up from `claim` frames — not inferred from the relay terminal — and appear even
9
- // with ZERO transcript. Each worker's drill `stream` is repointed at its claimed jobKey-scoped relay
10
- // stream (`job:<jobKey>`), keyed by the CLAIM (explicit instance+jobKey), not by a connection. The
9
+ // with ZERO transcript. Each worker's drill `stream` is repointed at its claimed instance-scoped relay
10
+ // stream (`composeStreamId(instance, jobKey)`, issue #738), keyed by the CLAIM (explicit
11
+ // instance+jobKey), not by a connection. The
11
12
  // relay correlation registry is DEMOTED to drill-in context only: it still supplies the `correlations`
12
13
  // — the process-instance / plan context for a job's terminal — so the cockpit lines a worker's terminal
13
14
  // up with "that process instance / this plan", but it is no longer the visibility source.
@@ -31,9 +32,10 @@ import { defineOperation } from "../nano-generated/operations.ts";
31
32
  const SECRET = envVar("NANO_PR_WEBHOOK_SECRET") ?? "";
32
33
 
33
34
  // Project a presence-registry row to the wire worker. The drill `stream` defaults to the worker
34
- // instance (H5) but is repointed at the worker's claimed jobKey-scoped relay stream (`job:<jobKey>`)
35
- // when the claim registry knows a current claim for it (#713) — keyed by the CLAIM, not by the
36
- // connection — so drilling in opens the LIVE job's terminal even before any transcript lands.
35
+ // instance (H5) but is repointed at the worker's claimed instance-scoped relay stream
36
+ // (`composeStreamId(instance, jobKey)`, issue #738) when the claim registry knows a current claim for
37
+ // it (#713) — keyed by the CLAIM, not by the connection — so drilling in opens the LIVE job's terminal
38
+ // (the exact stream the producer writes) even before any transcript lands.
37
39
  function toWorker(w: SupplyWorker, claims: ClaimRegistry | undefined): AgenticSupplyWorker {
38
40
  const out: AgenticSupplyWorker = {
39
41
  instance: w.instance,
@@ -9,6 +9,7 @@ import type { Frame } from "@nanobpm/agentic/protocol";
9
9
  import type { SqliteDb } from "@nanobpm/agentic/transcript";
10
10
  import type { AppApi, DataLayer } from "@nanobpm/urban";
11
11
  import { assert, assertEquals } from "#test-assert";
12
+ import { composeStreamId } from "@nanobpm/agentic/emit";
12
13
  import { createRelayFamily, currentRelayTranscriptService } from "../app/agentic/families/relay.family.ts";
13
14
  import type { AgenticContext } from "../app/agentic/registry.ts";
14
15
  import { noopLog } from "../test/log.ts";
@@ -82,17 +83,17 @@ test("returns the whole transcript from offset 0, then a resume slice from a lat
82
83
  const store = currentRelayTranscriptService()?.store;
83
84
  assert(store !== undefined);
84
85
  store.flush(
85
- "job:6494",
86
+ composeStreamId("wk", "6494"),
86
87
  { since: () => ({ entries: [{ offset: 0, chunk: "aa" }, { offset: 1, chunk: "bb" }, { offset: 2, chunk: "cc" }] }), nextOffset: 3 },
87
88
  "ephemeral",
88
89
  );
89
90
  try {
90
- const whole = (await handler(input("job:6494"), app)) as {
91
+ const whole = (await handler(input(composeStreamId("wk", "6494")), app)) as {
91
92
  status: number;
92
93
  body: { stream: string; from: number; gap: boolean; nextOffset: number; chunkCount: number; byteLength: number; entries: Array<{ offset: number; chunk: string }>; jobKey?: string };
93
94
  };
94
95
  assertEquals(whole.status, 200);
95
- assertEquals(whole.body.stream, "job:6494");
96
+ assertEquals(whole.body.stream, composeStreamId("wk", "6494"));
96
97
  assertEquals(whole.body.jobKey, "6494");
97
98
  assertEquals(whole.body.from, 0);
98
99
  assertEquals(whole.body.gap, false);
@@ -101,7 +102,7 @@ test("returns the whole transcript from offset 0, then a resume slice from a lat
101
102
  assertEquals(whole.body.byteLength, 6);
102
103
  assertEquals(whole.body.entries.map((e) => e.offset), [0, 1, 2]);
103
104
 
104
- const resume = (await handler(input("job:6494", { from: 2 }), app)) as {
105
+ const resume = (await handler(input(composeStreamId("wk", "6494"), { from: 2 }), app)) as {
105
106
  body: { from: number; chunkCount: number; entries: Array<{ offset: number; chunk: string }> };
106
107
  };
107
108
  assertEquals(resume.body.from, 2);
@@ -11,6 +11,7 @@ import type { Frame } from "@nanobpm/agentic/protocol";
11
11
  import type { SqliteDb } from "@nanobpm/agentic/transcript";
12
12
  import type { AppApi, DataLayer } from "@nanobpm/urban";
13
13
  import { assert, assertEquals } from "#test-assert";
14
+ import { composeStreamId } from "@nanobpm/agentic/emit";
14
15
  import { currentCorrelation } from "../app/agentic/correlation.ts";
15
16
  import { family as correlationFamily } from "../app/agentic/families/correlation.family.ts";
16
17
  import { createRelayFamily, currentRelayTranscriptService } from "../app/agentic/families/relay.family.ts";
@@ -78,8 +79,8 @@ test("projects the TranscriptStore rows into the list (byteLength, chunkCount, l
78
79
  relayFamily.mount(mountCtx(memSqlite()));
79
80
  const store = currentRelayTranscriptService()?.store;
80
81
  assert(store !== undefined, "the relay family installs a persisted store");
81
- // Seed one completed ephemeral session on a jobKey-scoped stream.
82
- store.flush("job:6494", { since: () => ({ entries: [{ offset: 0, chunk: "hello " }, { offset: 1, chunk: "world" }] }), nextOffset: 2 }, "ephemeral");
82
+ // Seed one completed ephemeral session on an instance-scoped stream (issue #738).
83
+ store.flush(composeStreamId("wk", "6494"), { since: () => ({ entries: [{ offset: 0, chunk: "hello " }, { offset: 1, chunk: "world" }] }), nextOffset: 2 }, "ephemeral");
83
84
  try {
84
85
  const res = (await handler(input(), app)) as {
85
86
  status: number;
@@ -89,8 +90,8 @@ test("projects the TranscriptStore rows into the list (byteLength, chunkCount, l
89
90
  assertEquals(res.body.count, 1);
90
91
  assert(typeof res.body.retentionMs === "number", "the list surfaces the retention window");
91
92
  const t = res.body.transcripts[0];
92
- assertEquals(t.stream, "job:6494");
93
- assertEquals(t.jobKey, "6494", "the jobKey is decoded from the job: stream id");
93
+ assertEquals(t.stream, composeStreamId("wk", "6494"));
94
+ assertEquals(t.jobKey, "6494", "the jobKey is decoded from the instance-scoped stream id");
94
95
  assertEquals(t.lifecycle, "ephemeral");
95
96
  assertEquals(t.status, "completed");
96
97
  assertEquals(t.chunkCount, 2);
@@ -105,7 +106,7 @@ test("enriches with the H6 correlation (process instance / plan) when it is stil
105
106
  relayFamily.mount(mountCtx(memSqlite()));
106
107
  const store = currentRelayTranscriptService()?.store;
107
108
  assert(store !== undefined);
108
- store.flush("job:6494", { since: () => ({ entries: [{ offset: 0, chunk: "x" }] }), nextOffset: 1 }, "ephemeral");
109
+ store.flush(composeStreamId("wk-a", "6494"), { since: () => ({ entries: [{ offset: 0, chunk: "x" }] }), nextOffset: 1 }, "ephemeral");
109
110
  correlationFamily.mount({ hub: undefined as never, registry: undefined as never, transport: undefined as never, data: undefined, log: noopLog() });
110
111
  currentCorrelation()?.link("wk-a", "6494", { processInstanceKey: "4612", bpmnProcessId: "plan-fanout", planKey: "o/r#142" });
111
112
  try {
@@ -124,18 +125,18 @@ test("filters by jobKey and plan", async () => {
124
125
  relayFamily.mount(mountCtx(memSqlite()));
125
126
  const store = currentRelayTranscriptService()?.store;
126
127
  assert(store !== undefined);
127
- store.flush("job:1", { since: () => ({ entries: [{ offset: 0, chunk: "a" }] }), nextOffset: 1 }, "ephemeral");
128
- store.flush("job:2", { since: () => ({ entries: [{ offset: 0, chunk: "b" }] }), nextOffset: 1 }, "ephemeral");
128
+ store.flush(composeStreamId("wk", "1"), { since: () => ({ entries: [{ offset: 0, chunk: "a" }] }), nextOffset: 1 }, "ephemeral");
129
+ store.flush(composeStreamId("wk", "2"), { since: () => ({ entries: [{ offset: 0, chunk: "b" }] }), nextOffset: 1 }, "ephemeral");
129
130
  correlationFamily.mount({ hub: undefined as never, registry: undefined as never, transport: undefined as never, data: undefined, log: noopLog() });
130
131
  currentCorrelation()?.link("wk", "2", { planKey: "o/r#9" });
131
132
  try {
132
133
  const byJob = (await handler(input({ jobKey: "1" }), app)) as { body: { count: number; transcripts: Array<Record<string, unknown>> } };
133
134
  assertEquals(byJob.body.count, 1);
134
- assertEquals(byJob.body.transcripts[0]?.stream, "job:1");
135
+ assertEquals(byJob.body.transcripts[0]?.stream, composeStreamId("wk", "1"));
135
136
 
136
137
  const byPlan = (await handler(input({ planKey: "o/r#9" }), app)) as { body: { count: number; transcripts: Array<Record<string, unknown>> } };
137
138
  assertEquals(byPlan.body.count, 1);
138
- assertEquals(byPlan.body.transcripts[0]?.stream, "job:2");
139
+ assertEquals(byPlan.body.transcripts[0]?.stream, composeStreamId("wk", "2"));
139
140
  } finally {
140
141
  correlationFamily.teardown?.();
141
142
  relayFamily.teardown?.();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/nano-workforce",
3
- "version": "0.183.0",
3
+ "version": "0.183.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",
@@ -27,6 +27,7 @@ import type { SqliteDb } from "@nanobpm/agentic/presence";
27
27
  import { decodeFrame, encodeFrame, type Frame } from "@nanobpm/agentic/protocol";
28
28
  import type { AppApi, DataLayer } from "@nanobpm/urban";
29
29
  import { assert, assertEquals } from "#test-assert";
30
+ import { composeStreamId } from "@nanobpm/agentic/emit";
30
31
  import { currentCorrelation } from "../app/agentic/correlation.ts";
31
32
  import { loadAgenticFamilies } from "../app/agentic/loader.ts";
32
33
  import { type AgenticContext, AgenticFamilyRegistry } from "../app/agentic/registry.ts";
@@ -146,7 +147,9 @@ function supplyInput() {
146
147
  test("E2E: the whole visibility plane wires up — presence, correlation, supply report, relay drill, and resume across a hub restart", async () => {
147
148
  const db = memSqlite();
148
149
  const JOB = "6494";
149
- const STREAM = `job:${JOB}`;
150
+ // The producer writes a job's terminal on the instance-scoped stream (issue #738); the drill stream
151
+ // the supply advertises MUST be this exact id, or the cockpit reads a stream that never existed.
152
+ const STREAM = composeStreamId("wk-a", JOB);
150
153
 
151
154
  // Sanity: the fleet the seam discovers really includes presence, relay, and correlation.
152
155
  const fleet = await mountFleet(db);