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 +15 -0
- package/agent-instance.mjs +35 -3
- package/c8ctl-plugin.js +79 -12
- package/package.json +8 -8
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
|
package/agent-instance.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
78
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.62.
|
|
79
|
-
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.62.
|
|
80
|
-
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.62.
|
|
81
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.62.
|
|
82
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.62.
|
|
83
|
-
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.62.
|
|
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
|
}
|