@nanobpm/nano-workforce 0.183.1 → 0.184.0

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/app/agentic/agent-history.test.ts +165 -0
  3. package/app/agentic/agent-history.ts +211 -0
  4. package/app/agentic/claim-registry.test.ts +3 -2
  5. package/app/agentic/claim-registry.ts +8 -6
  6. package/app/agentic/cockpit/agent-history-render.test.ts +85 -0
  7. package/app/agentic/cockpit/agent-history-render.ts +168 -0
  8. package/app/agentic/cockpit/agent-history-view.test.ts +104 -0
  9. package/app/agentic/cockpit/agent-history-view.ts +186 -0
  10. package/app/agentic/cockpit/index.ts +21 -0
  11. package/app/agentic/cockpit/mount.test.ts +133 -1
  12. package/app/agentic/cockpit/supply-boot-agent-history.test.ts +144 -0
  13. package/app/agentic/cockpit/supply-boot.ts +134 -1
  14. package/app/agentic/cockpit/supply-view.ts +3 -3
  15. package/app/agentic/cockpit/transcript-view.ts +1 -1
  16. package/app/agentic/correlation-store.test.ts +14 -10
  17. package/app/agentic/correlation-store.ts +4 -3
  18. package/app/agentic/correlation.test.ts +24 -16
  19. package/app/agentic/correlation.ts +25 -12
  20. package/app/agentic/families/claim.family.test.ts +2 -1
  21. package/app/agentic/families/relay.family.test.ts +74 -73
  22. package/app/agentic/families/relay.family.ts +24 -21
  23. package/app/agentic/transcript-read.test.ts +108 -17
  24. package/app/agentic/transcript-read.ts +41 -7
  25. package/app/contracts.ts +10 -2
  26. package/app/mcpToolSurface.ts +8 -1
  27. package/db/migrations/101_agentic_history_read_expand.sql +34 -0
  28. package/openapi.yaml +360 -0
  29. package/operations/agentHistoryEndpoints.test.ts +106 -0
  30. package/operations/getAgentInstanceHistory.ts +37 -0
  31. package/operations/getAgenticSupply.test.ts +41 -2
  32. package/operations/getAgenticSupply.ts +7 -5
  33. package/operations/getAgenticTranscript.test.ts +5 -4
  34. package/operations/listAgentInstances.ts +42 -0
  35. package/operations/listAgenticTranscripts.test.ts +10 -9
  36. package/package.json +3 -3
  37. package/pages/cockpit/cockpit.css +104 -0
  38. package/pages/cockpit/mount.js +286 -3
  39. package/test/agentic-e2e.test.ts +4 -1
@@ -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
  }
@@ -24,6 +24,7 @@ import {
24
24
  TRANSCRIPT_SCHEMA_SQL,
25
25
  } from "@nanobpm/agentic/transcript";
26
26
  import { assert, assertEquals, assertThrows } from "#test-assert";
27
+ import { composeStreamId } from "@nanobpm/agentic/emit";
27
28
  import { noopLog } from "../../../test/log.ts";
28
29
  import { CorrelationRegistry, jobStream } from "../correlation.ts";
29
30
  import { AgenticCorrelationStore } from "../correlation-store.ts";
@@ -349,15 +350,15 @@ test("H6 correlation write-side: a produce on job:<k> links instance→[k]; stre
349
350
  const { service, hub } = mkCorrelatedService(registry, memoryDb(), correlation, byConnection);
350
351
  const p = connect("prod", registry);
351
352
 
352
- // The first `produce` for a job-scoped stream links the producing worker instance → jobKey, from
353
+ // The first `produce` for an instance-scoped stream links the producing worker instance → jobKey, from
353
354
  // data already crossing the wire (jobKey decoded from the stream id; instance from the connection).
354
- hub.handler?.(produce(jobStream("k1"), 1, "chunk"), p.conn);
355
+ hub.handler?.(produce(composeStreamId("worker-A", "k1"), 1, "chunk"), p.conn);
355
356
  assertEquals(correlation.jobKeysFor("worker-A"), ["k1"], "instance → [jobKey] is now linked");
356
- assertEquals(correlation.resolve("k1")?.stream, jobStream("k1"), "context carries the job-scoped stream");
357
+ assertEquals(correlation.resolve("k1")?.stream, composeStreamId("worker-A", "k1"), "context carries the instance-scoped stream");
357
358
  assertEquals(correlation.count(), 1);
358
359
 
359
360
  // Job end (stream completion) releases the correlation, so the worker's supply row clears it.
360
- service.completeStream(jobStream("k1"));
361
+ service.completeStream(composeStreamId("worker-A", "k1"));
361
362
  assertEquals(correlation.jobKeysFor("worker-A"), [], "completion releases the job");
362
363
  assertEquals(correlation.resolve("k1"), undefined);
363
364
  assertEquals(correlation.count(), 0);
@@ -373,10 +374,10 @@ test("H6 correlation write-side: an unpersisted stream completion releases its j
373
374
  // producer — otherwise #reconcile keeps re-completing the stream on every subsequent frame.
374
375
  const { service, hub } = mkCorrelatedService(registry, undefined, correlation, byConnection);
375
376
  const p = connect("prod", registry);
376
- hub.handler?.(produce(jobStream("kU"), 1, "x"), p.conn);
377
+ hub.handler?.(produce(composeStreamId("worker-U", "kU"), 1, "x"), p.conn);
377
378
  assertEquals(correlation.jobKeysFor("worker-U"), ["kU"]);
378
379
 
379
- service.completeStream(jobStream("kU"));
380
+ service.completeStream(composeStreamId("worker-U", "kU"));
380
381
  assertEquals(correlation.jobKeysFor("worker-U"), [], "unpersisted completion releases the job");
381
382
  assertEquals(correlation.count(), 0);
382
383
 
@@ -393,16 +394,16 @@ test("H6 correlation write-side: a late produce after completion does not resurr
393
394
  const byConnection = new Map([["prod", "worker-R"]]);
394
395
  const { service, hub } = mkCorrelatedService(registry, undefined, correlation, byConnection);
395
396
  const p = connect("prod", registry);
396
- hub.handler?.(produce(jobStream("kR"), 1, "x"), p.conn);
397
+ hub.handler?.(produce(composeStreamId("worker-R", "kR"), 1, "x"), p.conn);
397
398
  assertEquals(correlation.jobKeysFor("worker-R"), ["kR"]);
398
399
 
399
400
  // Job end completes the stream and releases the correlation.
400
- service.completeStream(jobStream("kR"));
401
+ service.completeStream(composeStreamId("worker-R", "kR"));
401
402
  assertEquals(correlation.count(), 0, "completion releases the job");
402
403
 
403
404
  // A late `produce` frame for the same, still-live producer must NOT re-link/re-own the completed
404
405
  // stream — otherwise it resurrects a jobKey after it was released.
405
- hub.handler?.(produce(jobStream("kR"), 1, "late"), p.conn);
406
+ hub.handler?.(produce(composeStreamId("worker-R", "kR"), 1, "late"), p.conn);
406
407
  assertEquals(correlation.count(), 0, "a late produce does not resurrect a completed stream");
407
408
  assertEquals(correlation.jobKeysFor("worker-R"), [], "released jobKey stays released after a late produce");
408
409
  service.teardown();
@@ -420,20 +421,20 @@ test("H6 correlation write-side: a worker starting a NEW job over its live conne
420
421
  const p = connect("prod", registry);
421
422
 
422
423
  // Job 1: the worker relays k1's terminal → linked.
423
- hub.handler?.(produce(jobStream("k1"), 1, "job-1 line"), p.conn);
424
+ hub.handler?.(produce(composeStreamId("worker-A", "k1"), 1, "job-1 line"), p.conn);
424
425
  assertEquals(correlation.jobKeysFor("worker-A"), ["k1"], "job 1 is the current job");
425
426
 
426
427
  // Job 2 begins on the SAME live connection (no disconnect). The worker relaying k2's terminal PROVES
427
428
  // k1 finished (one job at a time) → k1 is superseded: released AND flushed to a durable past session.
428
- hub.handler?.(produce(jobStream("k2"), 1, "job-2 line"), p.conn);
429
+ hub.handler?.(produce(composeStreamId("worker-A", "k2"), 1, "job-2 line"), p.conn);
429
430
  assertEquals(correlation.jobKeysFor("worker-A"), ["k2"], "the supply row shows ONLY the current job — no accumulation");
430
431
  assertEquals(correlation.resolve("k1"), undefined, "the prior job's correlation is released");
431
432
  assertEquals(correlation.count(), 1);
432
433
 
433
434
  // The superseded job is not lost — its transcript is flushed and completed, i.e. a replayable past session.
434
- const priorMeta = service.transcriptOf(jobStream("k1"));
435
+ const priorMeta = service.transcriptOf(composeStreamId("worker-A", "k1"));
435
436
  assertEquals(priorMeta?.status, "completed", "the superseded job becomes a completed past session");
436
- assertEquals(service.reattach(jobStream("k1"), 0)?.entries.length, 1, "the past session replays its captured terminal");
437
+ assertEquals(service.reattach(composeStreamId("worker-A", "k1"), 0)?.entries.length, 1, "the past session replays its captured terminal");
437
438
  service.teardown();
438
439
  });
439
440
 
@@ -456,15 +457,15 @@ test("H6 durable attribution: completing/superseding a job persists the worker's
456
457
 
457
458
  // Job 1 runs, then the worker starts job 2 on the same live connection → job 1 is superseded and
458
459
  // released. Its attribution must be durably recorded BEFORE the live correlation forgets it.
459
- hub.handler?.(produce(jobStream("k1"), 1, "job-1 line"), p.conn);
460
- hub.handler?.(produce(jobStream("k2"), 1, "job-2 line"), p.conn);
460
+ hub.handler?.(produce(composeStreamId("worker-A", "k1"), 1, "job-1 line"), p.conn);
461
+ hub.handler?.(produce(composeStreamId("worker-A", "k2"), 1, "job-2 line"), p.conn);
461
462
 
462
463
  const durable = store.get("k1");
463
464
  assert(durable !== undefined, "the superseded job's attribution is persisted");
464
465
  assertEquals(durable?.instance, "worker-A");
465
466
  assertEquals(durable?.identity, "gpu-box-7");
466
467
  assertEquals(durable?.host, "us-east-1a");
467
- assertEquals(durable?.stream, jobStream("k1"));
468
+ assertEquals(durable?.stream, composeStreamId("worker-A", "k1"));
468
469
  assertEquals(durable?.completedAt, "2024-01-02T03:04:05.000Z");
469
470
  // The still-active job is NOT yet recorded (attribution is written on completion, not on link).
470
471
  assertEquals(store.get("k2"), undefined, "the active job has no completion attribution yet");
@@ -489,13 +490,13 @@ test("#544 element-instance enrichment: link-time resolution keys the completed
489
490
  const p = connect("prod", registry);
490
491
 
491
492
  // First produce links the job and fires the (async) element-instance resolution.
492
- hub.handler?.(produce(jobStream("k1"), 1, "job-1 line"), p.conn);
493
+ hub.handler?.(produce(composeStreamId("worker-A", "k1"), 1, "job-1 line"), p.conn);
493
494
  await tick();
494
495
  // The live correlation context is enriched while the job runs.
495
496
  assertEquals(correlation.resolve("k1")?.elementInstanceKey, "ei-1", "the live context carries the element instance");
496
497
 
497
498
  // Superseding with a new job completes k1 → its attribution persists WITH the element-instance key.
498
- hub.handler?.(produce(jobStream("k2"), 1, "job-2 line"), p.conn);
499
+ hub.handler?.(produce(composeStreamId("worker-A", "k2"), 1, "job-2 line"), p.conn);
499
500
  const durable = store.get("k1");
500
501
  assertEquals(durable?.elementInstanceKey, "ei-1", "the completed session is keyed on the element instance");
501
502
  service.teardown();
@@ -521,8 +522,8 @@ test("#544 element-instance enrichment: a resolution that lands AFTER completion
521
522
 
522
523
  // Link job k1 (fires the still-pending resolution), then supersede it → k1 completes and persists
523
524
  // its attribution BEFORE the element instance is known.
524
- hub.handler?.(produce(jobStream("k1"), 1, "job-1 line"), p.conn);
525
- hub.handler?.(produce(jobStream("k2"), 1, "job-2 line"), p.conn);
525
+ hub.handler?.(produce(composeStreamId("worker-A", "k1"), 1, "job-1 line"), p.conn);
526
+ hub.handler?.(produce(composeStreamId("worker-A", "k2"), 1, "job-2 line"), p.conn);
526
527
  assertEquals(store.get("k1")?.elementInstanceKey, undefined, "persisted before the element instance resolved");
527
528
  assertEquals(correlation.resolve("k1"), undefined, "k1's live correlation was already released");
528
529
 
@@ -544,9 +545,9 @@ test("#544 element-instance enrichment: an unresolved job (never parked) leaves
544
545
  resolveElementInstance: () => Promise.resolve(undefined),
545
546
  });
546
547
  const p = connect("prod", registry);
547
- hub.handler?.(produce(jobStream("k1"), 1, "job-1 line"), p.conn);
548
+ hub.handler?.(produce(composeStreamId("worker-A", "k1"), 1, "job-1 line"), p.conn);
548
549
  await tick();
549
- hub.handler?.(produce(jobStream("k2"), 1, "job-2 line"), p.conn);
550
+ hub.handler?.(produce(composeStreamId("worker-A", "k2"), 1, "job-2 line"), p.conn);
550
551
  const durable = store.get("k1");
551
552
  assert(durable !== undefined, "the session is still attributed");
552
553
  assertEquals(durable?.elementInstanceKey, undefined, "no element-instance key when the job was not resolvable");
@@ -580,14 +581,14 @@ test("#661 primary release: a terminal lifecycle event clears an idle-but-connec
580
581
  const p = connect("prod", registry);
581
582
 
582
583
  // The worker relays its job's terminal → linked as active.
583
- hub.handler?.(produce(jobStream("k1"), 1, "job-1 line"), p.conn);
584
+ hub.handler?.(produce(composeStreamId("worker-A", "k1"), 1, "job-1 line"), p.conn);
584
585
  assertEquals(correlation.jobKeysFor("worker-A"), ["k1"], "the job is active while it runs");
585
586
  assertEquals(correlation.count(), 1);
586
587
 
587
588
  // The job ends: the worker emits the terminal `lifecycle` event on the SAME live connection and then
588
589
  // goes idle — it does NOT disconnect and does NOT take a new job (no supersede). Before this fix that
589
590
  // finished job lingered forever as a phantom active job; now the terminal event releases it.
590
- hub.handler?.(lifecycle(jobStream("k1"), 1, "completed"), p.conn);
591
+ hub.handler?.(lifecycle(composeStreamId("worker-A", "k1"), 1, "completed"), p.conn);
591
592
  assertEquals(correlation.jobKeysFor("worker-A"), [], "the finished job is released on the terminal event");
592
593
  assertEquals(correlation.count(), 0, "count() drops — no phantom active job");
593
594
 
@@ -602,9 +603,9 @@ test("#661 primary release: a terminal lifecycle event clears an idle-but-connec
602
603
  assert(row.live, "the worker is still connected — the connection persists across jobs");
603
604
 
604
605
  // The terminal event itself is captured in the flushed transcript (release runs AFTER the ring append).
605
- const meta = service.transcriptOf(jobStream("k1"));
606
+ const meta = service.transcriptOf(composeStreamId("worker-A", "k1"));
606
607
  assertEquals(meta?.status, "completed", "the finished job becomes a completed past session");
607
- assertEquals(service.reattach(jobStream("k1"), 0)?.entries.length, 2, "the terminal event is part of the transcript");
608
+ assertEquals(service.reattach(composeStreamId("worker-A", "k1"), 0)?.entries.length, 2, "the terminal event is part of the transcript");
608
609
  service.teardown();
609
610
  });
610
611
 
@@ -615,14 +616,14 @@ test("#661 primary release: an `exited` lifecycle also releases; a non-terminal
615
616
  const { service, hub } = mkCorrelatedService(registry, memoryDb(), correlation, byConnection);
616
617
  const p = connect("prod", registry);
617
618
 
618
- hub.handler?.(produce(jobStream("k1"), 1, "job-1 line"), p.conn);
619
+ hub.handler?.(produce(composeStreamId("worker-A", "k1"), 1, "job-1 line"), p.conn);
619
620
  // A `phase: "open"` lifecycle is NOT terminal — a genuinely active job must not be cleared.
620
- hub.handler?.(lifecycle(jobStream("k1"), 1, "open"), p.conn);
621
+ hub.handler?.(lifecycle(composeStreamId("worker-A", "k1"), 1, "open"), p.conn);
621
622
  assertEquals(correlation.jobKeysFor("worker-A"), ["k1"], "an open lifecycle keeps the active job");
622
623
  assertEquals(correlation.count(), 1);
623
624
 
624
625
  // An `exited` lifecycle (a crash/kill the worker still managed to report) IS terminal → released.
625
- hub.handler?.(lifecycle(jobStream("k1"), 1, "exited"), p.conn);
626
+ hub.handler?.(lifecycle(composeStreamId("worker-A", "k1"), 1, "exited"), p.conn);
626
627
  assertEquals(correlation.jobKeysFor("worker-A"), [], "an exited lifecycle releases the job");
627
628
  assertEquals(correlation.count(), 0);
628
629
  service.teardown();
@@ -639,25 +640,25 @@ test("#710 close release: a job-end `phase:close` marker flushes the durable tra
639
640
  // job WITHOUT a supersede (no next job) and WITHOUT a disconnect (the channel persists across jobs),
640
641
  // so the ONLY job-end signal is the harness's drained `phase:close` marker.
641
642
  const N = 4;
642
- for (let i = 0; i < N; i++) hub.handler?.(produce(jobStream("k1"), 1, `job-1 line ${i}`), p.conn);
643
+ for (let i = 0; i < N; i++) hub.handler?.(produce(composeStreamId("worker-A", "k1"), 1, `job-1 line ${i}`), p.conn);
643
644
  assertEquals(correlation.jobKeysFor("worker-A"), ["k1"], "the job is active while it runs");
644
- assertEquals(service.transcriptOf(jobStream("k1")), undefined, "not flushed while the job runs");
645
+ assertEquals(service.transcriptOf(composeStreamId("worker-A", "k1")), undefined, "not flushed while the job runs");
645
646
 
646
647
  // The job completes: the harness drains its outbound relay buffer and emits `phase:close` on the SAME
647
648
  // live connection, then goes idle. Before #710 this was NOT a flush trigger — the durable transcript
648
649
  // was snapshotted late (or not at all) by a later supersede/disconnect, truncating the tail. Now the
649
650
  // close event flushes it deterministically at job-completion time.
650
- hub.handler?.(close(jobStream("k1"), 1), p.conn);
651
+ hub.handler?.(close(composeStreamId("worker-A", "k1"), 1), p.conn);
651
652
 
652
653
  assertEquals(correlation.jobKeysFor("worker-A"), [], "the finished job is released on the close marker");
653
654
  assertEquals(correlation.count(), 0, "no phantom active job after close");
654
655
 
655
- const meta = service.transcriptOf(jobStream("k1"));
656
+ const meta = service.transcriptOf(composeStreamId("worker-A", "k1"));
656
657
  assertEquals(meta?.status, "completed", "the job becomes a completed past session at close time");
657
658
  // ALL N produced frames PLUS the close marker itself are in the durable transcript — no truncated
658
659
  // tail (the close event rides the ring append before the release, like the #661 terminal path).
659
660
  assertEquals(
660
- service.reattach(jobStream("k1"), 0)?.entries.length,
661
+ service.reattach(composeStreamId("worker-A", "k1"), 0)?.entries.length,
661
662
  N + 1,
662
663
  "every relayed byte (and the close marker) is flushed — the tail is not truncated",
663
664
  );
@@ -708,12 +709,12 @@ test("#661 no-regression: an ordinary (non-lifecycle) chunk never clears a genui
708
709
  const { service, hub } = mkCorrelatedService(registry, memoryDb(), correlation, byConnection);
709
710
  const p = connect("prod", registry);
710
711
 
711
- hub.handler?.(produce(jobStream("k1"), 1, "job-1 line"), p.conn);
712
+ hub.handler?.(produce(composeStreamId("worker-A", "k1"), 1, "job-1 line"), p.conn);
712
713
  // Ordinary terminal output (raw bytes, not a typed envelope) must NOT be read as a job-end signal.
713
- for (let i = 0; i < 5; i++) hub.handler?.(produce(jobStream("k1"), 1, `output ${i}`), p.conn);
714
+ for (let i = 0; i < 5; i++) hub.handler?.(produce(composeStreamId("worker-A", "k1"), 1, `output ${i}`), p.conn);
714
715
  assertEquals(correlation.jobKeysFor("worker-A"), ["k1"], "a running job stays active across ordinary output");
715
716
  assertEquals(correlation.count(), 1);
716
- assertEquals(service.transcriptOf(jobStream("k1")), undefined, "the live job is not completed");
717
+ assertEquals(service.transcriptOf(composeStreamId("worker-A", "k1")), undefined, "the live job is not completed");
717
718
  service.teardown();
718
719
  });
719
720
 
@@ -732,7 +733,7 @@ test("#661 defensive reconcile: an unclean exit (no lifecycle) whose engine job
732
733
  });
733
734
  const p = connect("prod", registry);
734
735
 
735
- hub.handler?.(produce(jobStream("k1"), 1, "job-1 line"), p.conn);
736
+ hub.handler?.(produce(composeStreamId("worker-A", "k1"), 1, "job-1 line"), p.conn);
736
737
  assertEquals(correlation.jobKeysFor("worker-A"), ["k1"], "the job links while it runs");
737
738
 
738
739
  // While the engine still parks the job, the reconcile pass leaves a genuinely active job alone.
@@ -760,7 +761,7 @@ test("#661 defensive reconcile: a transient engine read failure never falsely re
760
761
  resolveElementInstance,
761
762
  });
762
763
  const p = connect("prod", registry);
763
- hub.handler?.(produce(jobStream("k1"), 1, "job-1 line"), p.conn);
764
+ hub.handler?.(produce(composeStreamId("worker-A", "k1"), 1, "job-1 line"), p.conn);
764
765
 
765
766
  await service.reconcileEngineCorrelations();
766
767
  assertEquals(correlation.jobKeysFor("worker-A"), ["k1"], "a transient engine failure leaves the job linked");
@@ -774,7 +775,7 @@ test("#661 defensive reconcile: a no-op when no engine resolver is wired", async
774
775
  const byConnection = new Map([["prod", "worker-A"]]);
775
776
  const { service, hub } = mkCorrelatedService(registry, memoryDb(), correlation, byConnection);
776
777
  const p = connect("prod", registry);
777
- hub.handler?.(produce(jobStream("k1"), 1, "job-1 line"), p.conn);
778
+ hub.handler?.(produce(composeStreamId("worker-A", "k1"), 1, "job-1 line"), p.conn);
778
779
 
779
780
  // With no element-instance resolver (engine-less host), the safety net cannot query the engine —
780
781
  // it must be an inert no-op, leaving the correlation exactly as the primary path manages it.
@@ -914,12 +915,12 @@ test("H6 correlation write-side: a throwing correlation.link() never escapes the
914
915
  // The first `produce` links — but the injected correlation throws. It must be swallowed (advisory),
915
916
  // so the frame handler does not throw and the stream is left UNLINKED so a later produce retries.
916
917
  failLink(true);
917
- hub.handler?.(produce(jobStream("kL"), 1, "x"), p.conn);
918
+ hub.handler?.(produce(composeStreamId("worker-L", "kL"), 1, "x"), p.conn);
918
919
  assertEquals(inner.count(), 0, "a throwing link is swallowed and records no correlation");
919
920
 
920
921
  // A later produce (link now succeeds) retries the link — proving the stream was left unlinked.
921
922
  failLink(false);
922
- hub.handler?.(produce(jobStream("kL"), 1, "y"), p.conn);
923
+ hub.handler?.(produce(composeStreamId("worker-L", "kL"), 1, "y"), p.conn);
923
924
  assertEquals(inner.jobKeysFor("worker-L"), ["kL"], "the link retries and succeeds on a later frame");
924
925
  assertEquals(inner.count(), 1);
925
926
  service.teardown();
@@ -940,7 +941,7 @@ test("H6 correlation write-side: a throwing correlation.releaseJob() never escap
940
941
  instanceForConnection: (id) => byConnection.get(id),
941
942
  });
942
943
  const p = connect("prod", registry);
943
- hub.handler?.(produce(jobStream("kX"), 1, "x"), p.conn);
944
+ hub.handler?.(produce(composeStreamId("worker-X", "kX"), 1, "x"), p.conn);
944
945
  assertEquals(inner.jobKeysFor("worker-X"), ["kX"]);
945
946
 
946
947
  // A producer disconnect drives #reconcile → #unlink, but releaseJob throws. The advisory contract
@@ -953,7 +954,7 @@ test("H6 correlation write-side: a throwing correlation.releaseJob() never escap
953
954
 
954
955
  // The stream still finalizes (terminal) so #reconcile does not thrash it every frame, and a later
955
956
  // frame's reconcile is a clean no-op rather than a repeated crash.
956
- hub.handler?.(produce(jobStream("kX"), 2, "late"), p.conn);
957
+ hub.handler?.(produce(composeStreamId("worker-X", "kX"), 2, "late"), p.conn);
957
958
  assertEquals(
958
959
  inner.jobKeysFor("worker-X"),
959
960
  ["kX"],
@@ -969,7 +970,7 @@ test("H6 correlation write-side: a producer disconnect releases its job correlat
969
970
  // No DataLayer → the relay runs unpersisted; correlation release must still fire (store-independent).
970
971
  const { service, hub } = mkCorrelatedService(registry, undefined, correlation, byConnection);
971
972
  const p = connect("prod", registry);
972
- hub.handler?.(produce(jobStream("k2"), 1, "x"), p.conn);
973
+ hub.handler?.(produce(composeStreamId("worker-B", "k2"), 1, "x"), p.conn);
973
974
  assertEquals(correlation.jobKeysFor("worker-B"), ["k2"]);
974
975
 
975
976
  // Producer drops; a subsequent frame from any live connection reconciles the dead producer.
@@ -1000,7 +1001,7 @@ test("#689 mid-job reconnect: a stale producer whose instance is still live keep
1000
1001
  isInstanceLive,
1001
1002
  });
1002
1003
  const p = connect("conn-old", registry);
1003
- hub.handler?.(produce(jobStream("5749"), 1, "booting agent"), p.conn);
1004
+ hub.handler?.(produce(composeStreamId("worker-L", "5749"), 1, "booting agent"), p.conn);
1004
1005
  assertEquals(correlation.jobKeysFor("worker-L"), ["5749"], "the job is linked on first produce");
1005
1006
 
1006
1007
  // The producer's WS connection blips (client reconnects). Model the reconnect faithfully: the new
@@ -1014,20 +1015,20 @@ test("#689 mid-job reconnect: a stale producer whose instance is still live keep
1014
1015
  const other = connect("cons", registry);
1015
1016
  hub.handler?.(grant(0), other.conn);
1016
1017
  assertEquals(correlation.jobKeysFor("worker-L"), ["5749"], "a mid-job reconnect keeps the correlation");
1017
- assertEquals(service.transcriptOf(jobStream("5749")), undefined, "the still-active stream is NOT archived");
1018
+ assertEquals(service.transcriptOf(composeStreamId("worker-L", "5749")), undefined, "the still-active stream is NOT archived");
1018
1019
 
1019
1020
  // The worker resumes producing on the SAME job over its already-open NEW connection → re-attributed,
1020
1021
  // still one job, transcript still live (not terminal).
1021
- hub.handler?.(produce(jobStream("5749"), 1, "resumed output"), p2.conn);
1022
+ hub.handler?.(produce(composeStreamId("worker-L", "5749"), 1, "resumed output"), p2.conn);
1022
1023
  assertEquals(correlation.jobKeysFor("worker-L"), ["5749"], "the resumed producer stays linked to the same job");
1023
- assertEquals(service.liveFallback(jobStream("5749"))?.ring !== undefined, true, "the transcript is still live, not completed");
1024
+ assertEquals(service.liveFallback(composeStreamId("worker-L", "5749"))?.ring !== undefined, true, "the transcript is still live, not completed");
1024
1025
 
1025
1026
  // Only once the worker truly EXITS (all its connections gone → isInstanceLive false) does a later
1026
1027
  // reconcile complete the stream and release the correlation.
1027
1028
  registry.remove("conn-new");
1028
1029
  hub.handler?.(grant(0), other.conn);
1029
1030
  assertEquals(correlation.jobKeysFor("worker-L"), [], "a true worker-exit completes + releases the job");
1030
- assertEquals(service.transcriptOf(jobStream("5749"))?.status, "completed", "the exited worker's stream is archived");
1031
+ assertEquals(service.transcriptOf(composeStreamId("worker-L", "5749"))?.status, "completed", "the exited worker's stream is archived");
1031
1032
  service.teardown();
1032
1033
  });
1033
1034
 
@@ -1039,14 +1040,14 @@ test("#689 mid-job reconnect: with no isInstanceLive wired, a producer disconnec
1039
1040
  // preserving the prior always-complete-on-disconnect behaviour for callers that don't wire presence.
1040
1041
  const { service, hub } = mkCorrelatedService(registry, memoryDb(), correlation, byConnection);
1041
1042
  const p = connect("prod", registry);
1042
- hub.handler?.(produce(jobStream("kN"), 1, "x"), p.conn);
1043
+ hub.handler?.(produce(composeStreamId("worker-N", "kN"), 1, "x"), p.conn);
1043
1044
  assertEquals(correlation.jobKeysFor("worker-N"), ["kN"]);
1044
1045
 
1045
1046
  registry.remove("prod");
1046
1047
  const other = connect("cons", registry);
1047
1048
  hub.handler?.(grant(0), other.conn);
1048
1049
  assertEquals(correlation.jobKeysFor("worker-N"), [], "disconnect completes + releases when liveness is unknown");
1049
- assertEquals(service.transcriptOf(jobStream("kN"))?.status, "completed");
1050
+ assertEquals(service.transcriptOf(composeStreamId("worker-N", "kN"))?.status, "completed");
1050
1051
  service.teardown();
1051
1052
  });
1052
1053
 
@@ -1065,7 +1066,7 @@ test("#691 engine-owned disconnect: a mid-job reconnect whose engine job is stil
1065
1066
  resolveElementInstance,
1066
1067
  });
1067
1068
  const p = connect("conn-old", registry);
1068
- hub.handler?.(produce(jobStream("9001"), 1, "booting agent"), p.conn);
1069
+ hub.handler?.(produce(composeStreamId("worker-E", "9001"), 1, "booting agent"), p.conn);
1069
1070
  assertEquals(correlation.jobKeysFor("worker-E"), ["9001"], "the job links on first produce");
1070
1071
 
1071
1072
  // The producer WS blips: the old connection drops (no new one yet — presence would report the
@@ -1075,20 +1076,20 @@ test("#691 engine-owned disconnect: a mid-job reconnect whose engine job is stil
1075
1076
  hub.handler?.(grant(0), other.conn);
1076
1077
  await tick(); // let the fire-and-forget engine reconcile settle
1077
1078
  assertEquals(correlation.jobKeysFor("worker-E"), ["9001"], "a still-parked job survives the disconnect");
1078
- assertEquals(service.transcriptOf(jobStream("9001")), undefined, "the still-active stream is NOT archived");
1079
- assertEquals(service.liveFallback(jobStream("9001"))?.ring !== undefined, true, "the transcript is still live");
1079
+ assertEquals(service.transcriptOf(composeStreamId("worker-E", "9001")), undefined, "the still-active stream is NOT archived");
1080
+ assertEquals(service.liveFallback(composeStreamId("worker-E", "9001"))?.ring !== undefined, true, "the transcript is still live");
1080
1081
 
1081
1082
  // The worker resumes producing on a NEW connection over the same job → re-attributed, still one job.
1082
1083
  byConnection.set("conn-new", "worker-E");
1083
1084
  const p2 = connect("conn-new", registry);
1084
- hub.handler?.(produce(jobStream("9001"), 1, "resumed output"), p2.conn);
1085
+ hub.handler?.(produce(composeStreamId("worker-E", "9001"), 1, "resumed output"), p2.conn);
1085
1086
  assertEquals(correlation.jobKeysFor("worker-E"), ["9001"], "the resumed producer stays linked to the same job");
1086
1087
 
1087
1088
  // The job genuinely ends: the engine park vanishes. The periodic backstop pass completes + archives it.
1088
1089
  parked = false;
1089
1090
  await service.reconcileEngineCorrelations();
1090
1091
  assertEquals(correlation.jobKeysFor("worker-E"), [], "the ended job is released");
1091
- assertEquals(service.transcriptOf(jobStream("9001"))?.status, "completed", "and its transcript is archived");
1092
+ assertEquals(service.transcriptOf(composeStreamId("worker-E", "9001"))?.status, "completed", "and its transcript is archived");
1092
1093
  service.teardown();
1093
1094
  });
1094
1095
 
@@ -1103,7 +1104,7 @@ test("#691 engine-owned disconnect: a true exit whose engine job is gone complet
1103
1104
  resolveElementInstance,
1104
1105
  });
1105
1106
  const p = connect("prod", registry);
1106
- hub.handler?.(produce(jobStream("kX"), 1, "x"), p.conn);
1107
+ hub.handler?.(produce(composeStreamId("worker-X", "kX"), 1, "x"), p.conn);
1107
1108
  assertEquals(correlation.jobKeysFor("worker-X"), ["kX"]);
1108
1109
 
1109
1110
  registry.remove("prod");
@@ -1111,7 +1112,7 @@ test("#691 engine-owned disconnect: a true exit whose engine job is gone complet
1111
1112
  hub.handler?.(grant(0), other.conn);
1112
1113
  await tick(); // the fire-and-forget engine reconcile resolves "gone" → completes
1113
1114
  assertEquals(correlation.jobKeysFor("worker-X"), [], "a truly-ended job is released on disconnect");
1114
- assertEquals(service.transcriptOf(jobStream("kX"))?.status, "completed", "and its transcript is archived");
1115
+ assertEquals(service.transcriptOf(composeStreamId("worker-X", "kX"))?.status, "completed", "and its transcript is archived");
1115
1116
  service.teardown();
1116
1117
  });
1117
1118
 
@@ -1132,7 +1133,7 @@ test("#691 engine-owned disconnect: an UNLINKED job stream (register/produce rac
1132
1133
  resolveElementInstance,
1133
1134
  });
1134
1135
  const p = connect("conn-old", registry);
1135
- hub.handler?.(produce(jobStream("7742"), 1, "booting agent"), p.conn);
1136
+ hub.handler?.(produce(composeStreamId("worker-R", "7742"), 1, "booting agent"), p.conn);
1136
1137
  assertEquals(correlation.count(), 0, "the stream did NOT link — the instance was not resolvable at produce time");
1137
1138
 
1138
1139
  // The old producer connection blips before a later produce could retry the link. A frame drives
@@ -1142,23 +1143,23 @@ test("#691 engine-owned disconnect: an UNLINKED job stream (register/produce rac
1142
1143
  hub.handler?.(grant(0), other.conn);
1143
1144
  await tick(); // let the fire-and-forget engine reconcile settle
1144
1145
  assertEquals(
1145
- service.transcriptOf(jobStream("7742")),
1146
+ service.transcriptOf(composeStreamId("worker-R", "7742")),
1146
1147
  undefined,
1147
1148
  "an unlinked-but-still-parked job is NOT archived on disconnect",
1148
1149
  );
1149
- assertEquals(service.liveFallback(jobStream("7742"))?.ring !== undefined, true, "its transcript is still live");
1150
+ assertEquals(service.liveFallback(composeStreamId("worker-R", "7742"))?.ring !== undefined, true, "its transcript is still live");
1150
1151
 
1151
1152
  // The worker resumes on a NEW connection; the instance now resolves → the late link finally lands.
1152
1153
  byConnection.set("conn-new", "worker-R");
1153
1154
  const p2 = connect("conn-new", registry);
1154
- hub.handler?.(produce(jobStream("7742"), 1, "resumed output"), p2.conn);
1155
+ hub.handler?.(produce(composeStreamId("worker-R", "7742"), 1, "resumed output"), p2.conn);
1155
1156
  assertEquals(correlation.jobKeysFor("worker-R"), ["7742"], "the late link succeeds on reconnect once the instance resolves");
1156
1157
 
1157
1158
  // The job genuinely ends: the engine park vanishes → the backstop pass completes + archives it.
1158
1159
  parked = false;
1159
1160
  await service.reconcileEngineCorrelations();
1160
1161
  assertEquals(correlation.jobKeysFor("worker-R"), [], "the ended job is released");
1161
- assertEquals(service.transcriptOf(jobStream("7742"))?.status, "completed", "and its transcript is archived");
1162
+ assertEquals(service.transcriptOf(composeStreamId("worker-R", "7742"))?.status, "completed", "and its transcript is archived");
1162
1163
  service.teardown();
1163
1164
  });
1164
1165
 
@@ -1180,7 +1181,7 @@ test("#708 periodic backstop covers an UNLINKED job stream: an unlinked-but-park
1180
1181
  resolveElementInstance,
1181
1182
  });
1182
1183
  const p = connect("conn-old", registry);
1183
- hub.handler?.(produce(jobStream("7708"), 1, "booting agent"), p.conn);
1184
+ hub.handler?.(produce(composeStreamId("wk", "7708"), 1, "booting agent"), p.conn);
1184
1185
  assertEquals(correlation.count(), 0, "the stream did NOT link — the instance was not resolvable at produce time");
1185
1186
 
1186
1187
  // Producer blips; the disconnect engine-reconcile keeps the still-parked stream live and clears
@@ -1190,7 +1191,7 @@ test("#708 periodic backstop covers an UNLINKED job stream: an unlinked-but-park
1190
1191
  hub.handler?.(grant(0), other.conn);
1191
1192
  await tick();
1192
1193
  assertEquals(
1193
- service.transcriptOf(jobStream("7708")),
1194
+ service.transcriptOf(composeStreamId("wk", "7708")),
1194
1195
  undefined,
1195
1196
  "an unlinked-but-still-parked job is NOT archived on disconnect",
1196
1197
  );
@@ -1200,11 +1201,11 @@ test("#708 periodic backstop covers an UNLINKED job stream: an unlinked-but-park
1200
1201
  parked = false;
1201
1202
  await service.reconcileEngineCorrelations();
1202
1203
  assertEquals(
1203
- service.transcriptOf(jobStream("7708"))?.status,
1204
+ service.transcriptOf(composeStreamId("wk", "7708"))?.status,
1204
1205
  "completed",
1205
1206
  "the periodic backstop completes an unlinked job stream once the engine park disappears",
1206
1207
  );
1207
- assertEquals(service.liveFallback(jobStream("7708")), undefined, "and its live ring is retired");
1208
+ assertEquals(service.liveFallback(composeStreamId("wk", "7708")), undefined, "and its live ring is retired");
1208
1209
  service.teardown();
1209
1210
  });
1210
1211
 
@@ -1224,21 +1225,21 @@ test("#691 engine-owned disconnect: a transient engine read at disconnect never
1224
1225
  resolveElementInstance,
1225
1226
  });
1226
1227
  const p = connect("prod", registry);
1227
- hub.handler?.(produce(jobStream("kT"), 1, "x"), p.conn);
1228
+ hub.handler?.(produce(composeStreamId("worker-T", "kT"), 1, "x"), p.conn);
1228
1229
 
1229
1230
  registry.remove("prod");
1230
1231
  const other = connect("cons", registry);
1231
1232
  hub.handler?.(grant(0), other.conn);
1232
1233
  await tick(); // the engine read rejects → the stream must stay linked, not complete
1233
1234
  assertEquals(correlation.jobKeysFor("worker-T"), ["kT"], "a transient engine failure leaves the job linked");
1234
- assertEquals(service.transcriptOf(jobStream("kT")), undefined, "and its transcript is NOT archived");
1235
+ assertEquals(service.transcriptOf(composeStreamId("worker-T", "kT")), undefined, "and its transcript is NOT archived");
1235
1236
 
1236
1237
  // The engine recovers and now reports the job gone: the periodic backstop pass completes it.
1237
1238
  failing = false;
1238
1239
  gone = true;
1239
1240
  await service.reconcileEngineCorrelations();
1240
1241
  assertEquals(correlation.jobKeysFor("worker-T"), [], "the backstop pass releases the ended job");
1241
- assertEquals(service.transcriptOf(jobStream("kT"))?.status, "completed", "and archives its transcript");
1242
+ assertEquals(service.transcriptOf(composeStreamId("worker-T", "kT"))?.status, "completed", "and archives its transcript");
1242
1243
  service.teardown();
1243
1244
  });
1244
1245
 
@@ -1255,10 +1256,10 @@ test("H6 correlation write-side: non-job streams are never linked; a link retrie
1255
1256
 
1256
1257
  // A job stream whose producer's presence instance is not yet known does not link — but a later
1257
1258
  // frame (after the register lands) retries and links, closing the register/produce race.
1258
- hub.handler?.(produce(jobStream("k3"), 1, "a"), p.conn);
1259
+ hub.handler?.(produce(composeStreamId("worker-C", "k3"), 1, "a"), p.conn);
1259
1260
  assertEquals(correlation.count(), 0, "no presence instance yet → not linked");
1260
1261
  byConnection.set("prod", "worker-C");
1261
- hub.handler?.(produce(jobStream("k3"), 1, "b"), p.conn);
1262
+ hub.handler?.(produce(composeStreamId("worker-C", "k3"), 1, "b"), p.conn);
1262
1263
  assertEquals(correlation.jobKeysFor("worker-C"), ["k3"], "retried link succeeds once the instance resolves");
1263
1264
  service.teardown();
1264
1265
  });