c8ctl-plugin-nano 1.62.0 → 1.62.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/README.md CHANGED
@@ -197,6 +197,21 @@ capabilities `code-review, testing` the matrix is:
197
197
  so a BPMN service task can target a worker at any granularity by setting its job
198
198
  type to the matching token.
199
199
 
200
+ **Choosing which engine a worker connects to.** By default `work` (and
201
+ `supervisor start --worker`) connects to your **active** c8ctl session profile
202
+ (`c8ctl use profile <name>`). To point a single invocation at a different
203
+ cluster without switching the active session, pass c8ctl's global
204
+ `--profile <name>` — it is honoured the same way it is for core c8ctl commands:
205
+
206
+ ```bash
207
+ c8ctl nano work fleet --profile nano-validate # this worker only → nano-validate's engine
208
+ c8ctl nano supervisor start --worker fleet --profile nano-validate # the whole fleet → nano-validate
209
+ ```
210
+
211
+ The named profile wins over the active session for that run only; a supervised
212
+ fleet is pinned to it (the daemon forwards it to every worker it spawns), and
213
+ `supervisor status` reports the matching `ENGINE`.
214
+
200
215
  To also service a job type the matrix can't express — for example a code-first
201
216
  [`@nanobpm/workflow`](https://www.npmjs.com/package/@nanobpm/workflow) flow whose
202
217
  external task type is `<flowId>:<taskName>`, or any bespoke token — add one or
@@ -311,6 +311,23 @@ function shortHash(text) {
311
311
  return (h >>> 0).toString(36);
312
312
  }
313
313
 
314
+ // #247: the per-ACTIVATION namespace folded into every content-derived historyItemId
315
+ // so a resumed harness (which restarts its ACP message / tool-call numbering) does not
316
+ // collide with — and get silently deduplicated against — the prior activation's turns.
317
+ // The activation identity is the lease token (a fresh token per `activate jobs`), else
318
+ // the jobKey, else the elementInstanceKey. It is HASHED — the lease token is a
319
+ // secret-ish fence token that must never be embedded raw in a persisted id — and stays
320
+ // STABLE for the life of one activation, so an at-least-once redelivery still dedups
321
+ // within the activation while a genuine resume (new lease) gets a distinct namespace.
322
+ // Returns '' when no identity is available, leaving the id un-namespaced (unchanged).
323
+ export function activationNamespace(job = {}) {
324
+ const leaseToken = job?.leaseToken != null ? String(job.leaseToken) : '';
325
+ const jobKey = job?.jobKey != null ? String(job.jobKey) : '';
326
+ const elementInstanceKey = job?.elementInstanceKey != null ? String(job.elementInstanceKey) : '';
327
+ const token = leaseToken || jobKey || elementInstanceKey || '';
328
+ return token ? shortHash(token) : '';
329
+ }
330
+
314
331
  // Extract per-call metrics from an ACP update when the agent carries them (many
315
332
  // ACP agents do not — a documented ACP fidelity gap — so this is usually absent).
316
333
  // Reads the common usage locations and maps to the AgentHistory item metric field
@@ -408,6 +425,21 @@ export function createAgentInstanceProducer(opts = {}) {
408
425
  const elementInstanceKey = job?.elementInstanceKey != null ? String(job.elementInstanceKey) : '';
409
426
  const elementId = job?.elementId != null ? String(job.elementId) : null;
410
427
  const processInstanceKey = job?.processInstanceKey != null ? String(job.processInstanceKey) : '';
428
+ // #247: namespace every content-derived historyItemId per ACTIVATION. When a parked
429
+ // process resumes, a fresh (cold-started) harness re-activates the SAME AgentInstance
430
+ // and generally RESTARTS its ACP message / tool-call numbering (m-1, call-1, …). The
431
+ // engine dedups appends by historyItemId, so without a per-activation namespace those
432
+ // continuation turns collide with the PRIOR activation's ids and are silently dropped
433
+ // as stale history — the transcript never advances, and the next reactivation replays
434
+ // the same side effects (the failure mode #239 exists to prevent). The namespace is
435
+ // derived from the job's ACTIVATION identity (see `activationNamespace`): stable for
436
+ // the life of this producer, so an at-least-once redelivery of the SAME activation
437
+ // still dedups correctly, while a genuine resume (new lease → new namespace) appends
438
+ // instead of colliding. The CONFIGURATION turn is left keyed on the elementInstanceKey
439
+ // ALONE (stable across activations) so its single header dedups per instance rather
440
+ // than duplicating on every resume.
441
+ const activationNs = activationNamespace(job);
442
+ const nsHistoryId = (id) => (activationNs ? `${activationNs}:${id}` : id);
411
443
  // #229 cross-channel correlation, compact form. Stamps job/eik/pik so the
412
444
  // AgentInstance channel can be joined to the job / relay / git channels. This is the
413
445
  // terse rendering used by the observability lines; `correlation()` below is the
@@ -637,7 +669,7 @@ export function createAgentInstanceProducer(opts = {}) {
637
669
  if (!hasText && !hasMetrics) return;
638
670
  const idBasis = isNonBlank(msg.messageId) ? String(msg.messageId) : `h:${shortHash(text)}`;
639
671
  const turn = {
640
- historyItemId: `${msg.role.toLowerCase()}:${idBasis}`,
672
+ historyItemId: nsHistoryId(`${msg.role.toLowerCase()}:${idBasis}`),
641
673
  loopIteration: msg.loopIteration,
642
674
  role: msg.role,
643
675
  content: hasText ? [{ contentType: 'TEXT', text }] : [],
@@ -653,7 +685,7 @@ export function createAgentInstanceProducer(opts = {}) {
653
685
  flushMessage();
654
686
  if (isNonBlank(c.name)) toolNames.set(String(c.callId), String(c.name));
655
687
  const turn = {
656
- historyItemId: `toolcall:${c.callId}`,
688
+ historyItemId: nsHistoryId(`toolcall:${c.callId}`),
657
689
  loopIteration,
658
690
  role: 'ASSISTANT',
659
691
  content: [],
@@ -673,7 +705,7 @@ export function createAgentInstanceProducer(opts = {}) {
673
705
  const onToolResult = (c) => {
674
706
  flushMessage();
675
707
  const turn = {
676
- historyItemId: `toolresult:${c.callId}`,
708
+ historyItemId: nsHistoryId(`toolresult:${c.callId}`),
677
709
  loopIteration,
678
710
  role: 'TOOL_RESULT',
679
711
  content: contentForResult(c.result),
package/c8ctl-plugin.js CHANGED
@@ -8568,7 +8568,7 @@ function checkSetupAbort(abortSignal, { jobType, jobKey, stage, logger } = {}) {
8568
8568
  * the rank×capability matrix) and poll for work in the foreground until Ctrl-C.
8569
8569
  * Uses the c8ctl-provided SDK client (globalThis.c8ctl.createClient()).
8570
8570
  */
8571
- async function workAgent(req, flags) {
8571
+ async function workAgent(req, flags, ctx) {
8572
8572
  const logger = getLogger();
8573
8573
  // The hire to run always comes from the positional profile. `--name` no longer
8574
8574
  // selects the hire (that was a footgun: `work reviewer --name coder` silently
@@ -8850,7 +8850,14 @@ async function workAgent(req, flags) {
8850
8850
  // bad DNS answer is corrected, without a supervisor restart. Set before the
8851
8851
  // client is created so every outbound inherits it.
8852
8852
  preferIpv4Resolution();
8853
- const camunda = globalThis.c8ctl.createClient();
8853
+ // Honour c8ctl's global `--profile <name>` for this invocation: the handler
8854
+ // ctx carries `profile` (the `--profile` override, else the active session
8855
+ // profile). Passing it to createClient(profile) connects the worker to the
8856
+ // profile named on the command line, not just the active session — closing the
8857
+ // silently-ignored-`--profile` gap (jwulf/c8ctl-plugin-nano#189). ctx-less
8858
+ // callers (undefined) fall back to createClient(undefined), which resolves the
8859
+ // active profile itself — identical to the old no-arg behaviour.
8860
+ const camunda = globalThis.c8ctl.createClient(resolveConnectionProfile(ctx));
8854
8861
 
8855
8862
  // Broker REST endpoint for live linked-resource prompts (issue #63) and the
8856
8863
  // C8 REST source for `--auto`'s engine-read enrolment. Derived from the SAME
@@ -10315,6 +10322,55 @@ function reconstructWorkArgs(flags) {
10315
10322
  return out;
10316
10323
  }
10317
10324
 
10325
+ // The c8ctl connection profile (its global `--profile` flag) resolved for THIS
10326
+ // invocation, for handing to `createClient(profile)`. c8ctl core builds the
10327
+ // plugin handler's third `ctx` argument with `ctx.profile = --profile override
10328
+ // ?? activeProfile` (index.js), then exposes a lazy `createClient(pluginProfile)`
10329
+ // — but this plugin creates its own client via `globalThis.c8ctl.createClient()`
10330
+ // and, before this, called it with NO profile, so `work`/`supervisor` always
10331
+ // connected to the ACTIVE session profile and silently ignored a per-invocation
10332
+ // `--profile <name>` (jwulf/c8ctl-plugin-nano#189). Threading this value into
10333
+ // `createClient(profile)` makes them honour `--profile` the way core c8ctl
10334
+ // commands do. Returns undefined when ctx carries no profile, so
10335
+ // `createClient(undefined)` resolves the active session profile itself (its own
10336
+ // documented default) — byte-identical to the old no-arg call. Pure.
10337
+ function resolveConnectionProfile(ctx) {
10338
+ const p = ctx && typeof ctx.profile === 'string' ? ctx.profile.trim() : '';
10339
+ return p || undefined;
10340
+ }
10341
+
10342
+ // The EXPLICIT `--profile <name>` override only (distinct from the active
10343
+ // session profile), for FORWARDING to spawned `nano work` children. When an
10344
+ // operator runs `supervisor start --worker <p> --profile <conn>` (or
10345
+ // `supervisor add … --profile <conn>`), each supervised worker is a fresh
10346
+ // `c8ctl nano work` process that must connect to <conn>, not the daemon's/
10347
+ // session's active profile. c8ctl strips the global `--profile` before the
10348
+ // plugin parser sees it, so it never lands in `flags`; we recover it from
10349
+ // `ctx.profile` and re-emit it as a `--profile` token in the child argv (c8ctl
10350
+ // core parses it position-independently as a global). Returns undefined when no
10351
+ // override was given — i.e. `ctx.profile` is absent or merely equals the active
10352
+ // session profile — so the child inherits the active profile exactly as before
10353
+ // (no spurious pin). Pure.
10354
+ function explicitConnectionProfile(ctx) {
10355
+ const p = resolveConnectionProfile(ctx);
10356
+ if (!p) return undefined;
10357
+ const active = globalThis.c8ctl && typeof globalThis.c8ctl.activeProfile === 'string'
10358
+ ? globalThis.c8ctl.activeProfile
10359
+ : undefined;
10360
+ return p === active ? undefined : p;
10361
+ }
10362
+
10363
+ // Append the explicit connection-profile override (if any) to a reconstructed
10364
+ // `work` argv tail as a c8ctl global `--profile <conn>` token, so a supervised
10365
+ // worker connects to the profile named on the `supervisor start`/`add` command
10366
+ // line rather than the active session profile (jwulf/c8ctl-plugin-nano#189).
10367
+ // A no-op when no `--profile` override was passed. Pure.
10368
+ function withConnectionProfileArg(workArgs, ctx) {
10369
+ const conn = explicitConnectionProfile(ctx);
10370
+ const base = Array.isArray(workArgs) ? workArgs : [];
10371
+ return conn ? [...base, '--profile', conn] : base;
10372
+ }
10373
+
10318
10374
  /**
10319
10375
  * Sanitize one token for use inside a worker name: keep `[A-Za-z0-9._-]`,
10320
10376
  * collapse every other run to a single `-`, and trim leading/trailing
@@ -11737,13 +11793,17 @@ async function startSupervisorWithServicePolicy(logger = getLogger()) {
11737
11793
  return startSupervisorDaemon({ adoptOnly: serviceOwned });
11738
11794
  }
11739
11795
 
11740
- async function supervisorStartCmd(req, flags) {
11796
+ async function supervisorStartCmd(req, flags, ctx) {
11741
11797
  const logger = getLogger();
11742
11798
  const state = await startSupervisorWithServicePolicy(logger);
11743
11799
  logger.info(`Supervisor daemon running (pid ${state.pid}).`);
11744
11800
 
11745
11801
  const specs = normalizeArgList(flags?.worker);
11746
- const workArgs = reconstructWorkArgs(flags);
11802
+ // Forward c8ctl's global `--profile <conn>` (when it overrides the active
11803
+ // session profile) to every spawned worker, so `supervisor start --worker <p>
11804
+ // --profile <conn>` pins the fleet to <conn> instead of silently connecting to
11805
+ // the active session engine (jwulf/c8ctl-plugin-nano#189).
11806
+ const workArgs = withConnectionProfileArg(reconstructWorkArgs(flags), ctx);
11747
11807
  // `--name` names a single launched worker. With several `--worker` specs a lone
11748
11808
  // name can't apply to all of them, so honour it only for a single spec and let
11749
11809
  // the rest auto-name; warn so the intent isn't silently dropped.
@@ -11803,7 +11863,7 @@ async function supervisorStatusCmd() {
11803
11863
  printSupervisorStatus(logger, statusFromState(running));
11804
11864
  }
11805
11865
 
11806
- async function supervisorAddCmd(req, flags) {
11866
+ async function supervisorAddCmd(req, flags, ctx) {
11807
11867
  const logger = getLogger();
11808
11868
  // The positional profile is what runs; `--name` names this worker instance
11809
11869
  // (forwarded to the child as `nano work … --name`, and used as its supervisor
@@ -11822,7 +11882,10 @@ async function supervisorAddCmd(req, flags) {
11822
11882
  process.exit(1);
11823
11883
  }
11824
11884
  await startSupervisorWithServicePolicy(logger);
11825
- const workArgs = reconstructWorkArgs(flags);
11885
+ // Forward the global `--profile <conn>` override to the spawned worker(s) so
11886
+ // `supervisor add … --profile <conn>` connects them to <conn>, matching
11887
+ // `supervisor start` (jwulf/c8ctl-plugin-nano#189).
11888
+ const workArgs = withConnectionProfileArg(reconstructWorkArgs(flags), ctx);
11826
11889
  let added = 0;
11827
11890
  let failed = 0;
11828
11891
  for (let i = 0; i < count; i++) {
@@ -12617,7 +12680,7 @@ async function maybeReparentOrWarnOnStart(logger) {
12617
12680
  }
12618
12681
 
12619
12682
  /** Dispatch the `supervisor` subcommand's action. */
12620
- async function supervisorCommand(req, flags) {
12683
+ async function supervisorCommand(req, flags, ctx) {
12621
12684
  const action = (req.positional[0] || '').toLowerCase();
12622
12685
  switch (action) {
12623
12686
  case '__daemon':
@@ -12632,7 +12695,7 @@ async function supervisorCommand(req, flags) {
12632
12695
  return;
12633
12696
  }
12634
12697
  case 'start':
12635
- await supervisorStartCmd(req, flags);
12698
+ await supervisorStartCmd(req, flags, ctx);
12636
12699
  return;
12637
12700
  case 'install':
12638
12701
  await supervisorInstallCmd();
@@ -12646,7 +12709,7 @@ async function supervisorCommand(req, flags) {
12646
12709
  await supervisorStatusCmd();
12647
12710
  return;
12648
12711
  case 'add':
12649
- await supervisorAddCmd(req, flags);
12712
+ await supervisorAddCmd(req, flags, ctx);
12650
12713
  return;
12651
12714
  case 'remove':
12652
12715
  case 'rm':
@@ -15463,6 +15526,9 @@ export {
15463
15526
  };
15464
15527
  export {
15465
15528
  reconstructWorkArgs,
15529
+ resolveConnectionProfile,
15530
+ explicitConnectionProfile,
15531
+ withConnectionProfileArg,
15466
15532
  supervisorWorkerId,
15467
15533
  autoWorkerName,
15468
15534
  sanitizeNameToken,
@@ -15498,6 +15564,7 @@ export {
15498
15564
  runSupervisorDaemon,
15499
15565
  startSupervisorDaemon,
15500
15566
  supervisorRequest,
15567
+ supervisorStartCmd,
15501
15568
  supervisorAddCmd,
15502
15569
  runningSupervisor,
15503
15570
  readSupervisorState,
@@ -15687,7 +15754,7 @@ export const commands = {
15687
15754
  manifest: { type: 'string', description: `workforce: manifest name to operate on (default ${DEFAULT_WORKFORCE_MANIFEST}); each subcommand reads/writes <stateHome>/workforce/<name>.json. Renamed from --profile (which now collides with c8ctl's global connection-profile flag).` },
15688
15755
  roles: { type: 'string', description: 'workforce add: comma-separated role list for the entry (→ --job-type <rank>:<role> at start); mutually exclusive with --auto' },
15689
15756
  },
15690
- handler: async (args, flags) => {
15757
+ handler: async (args, flags, ctx) => {
15691
15758
  const logger = getLogger();
15692
15759
  const req = parseRequest(args, flags);
15693
15760
 
@@ -15744,10 +15811,10 @@ export const commands = {
15744
15811
  await assignCapabilities(req, flags);
15745
15812
  break;
15746
15813
  case 'work':
15747
- await workAgent(req, flags);
15814
+ await workAgent(req, flags, ctx);
15748
15815
  break;
15749
15816
  case 'supervisor':
15750
- await supervisorCommand(req, flags);
15817
+ await supervisorCommand(req, flags, ctx);
15751
15818
  break;
15752
15819
  case 'workforce':
15753
15820
  await workforceCommand(req, flags);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c8ctl-plugin-nano",
3
- "version": "1.62.0",
3
+ "version": "1.62.2",
4
4
  "type": "module",
5
5
  "description": "c8ctl plugin to start, inspect, and stop a local Nano BPM (nanobpmn) cluster",
6
6
  "main": "c8ctl-plugin.js",
@@ -74,12 +74,12 @@
74
74
  },
75
75
  "optionalDependencies": {
76
76
  "node-pty": "^1.0.0",
77
- "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.62.0",
78
- "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.62.0",
79
- "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.62.0",
80
- "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.62.0",
81
- "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.62.0",
82
- "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.62.0",
83
- "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.62.0"
77
+ "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.62.2",
78
+ "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.62.2",
79
+ "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.62.2",
80
+ "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.62.2",
81
+ "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.62.2",
82
+ "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.62.2",
83
+ "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.62.2"
84
84
  }
85
85
  }