@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
|
@@ -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, "
|
|
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, "
|
|
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
|
|
10
|
-
// stream (`
|
|
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
|
|
35
|
-
// when the claim registry knows a current claim for
|
|
36
|
-
// connection — so drilling in opens the LIVE job's terminal
|
|
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
|
-
"
|
|
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("
|
|
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, "
|
|
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("
|
|
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
|
|
82
|
-
store.flush("
|
|
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, "
|
|
93
|
-
assertEquals(t.jobKey, "6494", "the jobKey is decoded from the
|
|
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("
|
|
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("
|
|
128
|
-
store.flush("
|
|
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, "
|
|
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, "
|
|
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.
|
|
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",
|
package/test/agentic-e2e.test.ts
CHANGED
|
@@ -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
|
-
|
|
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);
|