c8ctl-plugin-nano 1.36.2 → 1.37.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.
- package/README.md +21 -0
- package/c8ctl-plugin.js +204 -4
- package/package.json +8 -8
- package/work-channel.mjs +7 -0
package/README.md
CHANGED
|
@@ -324,6 +324,27 @@ used **verbatim** — so an explicit target always wins, and it's also how you
|
|
|
324
324
|
disambiguate when several apps are running. `NANO_AGENTIC=off` disables the
|
|
325
325
|
channel entirely and attempts **no** discovery.
|
|
326
326
|
|
|
327
|
+
**Seeing it work — `supervisor status`.** Supervised workers report both the
|
|
328
|
+
engine they poll and their agentic-channel state to `c8ctl nano supervisor
|
|
329
|
+
status` (and the interactive console), so you don't have to read raw worker logs
|
|
330
|
+
to tell whether presence actually reached the Workforce hub. The table gains an
|
|
331
|
+
`ENGINE` column (the engine's `host:port`) and an `AGENTIC` column whose value is
|
|
332
|
+
one of:
|
|
333
|
+
|
|
334
|
+
| AGENTIC | meaning |
|
|
335
|
+
| --- | --- |
|
|
336
|
+
| `starting` | transient: the worker just spawned and hasn't resolved its channel target yet (pre-`connecting`) |
|
|
337
|
+
| `connected` | presence is live on the hub — you should see this worker in the Cockpit |
|
|
338
|
+
| `connecting` | resolved a hub, socket not open yet (or the hub is unreachable) |
|
|
339
|
+
| `disconnected` | an established channel dropped (hub restart/outage) — it auto-reconnects; also set if the channel failed to start (bad URL/refused socket), in which case it stays disconnected until the worker restarts |
|
|
340
|
+
| `advisory` | nothing discoverable at the engine — **not** in the Cockpit; set `NANO_AGENTIC_URL` |
|
|
341
|
+
| `off` | visibility disabled (`NANO_AGENTIC=off`) |
|
|
342
|
+
| `?` | a live worker not yet reporting, or an older build predating these fields |
|
|
343
|
+
|
|
344
|
+
If workers show `advisory` (or stay `connecting`) while jobs still run, that's the
|
|
345
|
+
"connected to the engine but empty Cockpit" case: point them at the app with
|
|
346
|
+
`export NANO_AGENTIC_URL=http://<engine-host>:<appUi.port>` (e.g. `:3000`).
|
|
347
|
+
|
|
327
348
|
**Secure mode (opt-in).** For a deployment where you want the visibility channel
|
|
328
349
|
authenticated (rather than open on the LAN), start the server **and** every worker
|
|
329
350
|
box with the **same** `NANO_AGENTIC_SECRET` — same env-var name, same value on both
|
package/c8ctl-plugin.js
CHANGED
|
@@ -4407,6 +4407,93 @@ async function resolveAgenticTarget(opts = {}) {
|
|
|
4407
4407
|
};
|
|
4408
4408
|
}
|
|
4409
4409
|
|
|
4410
|
+
/**
|
|
4411
|
+
* Collapse an agentic disconnect/failure detail into the single short string the
|
|
4412
|
+
* marker's `agentic.message` field carries (#99 contract). Accepts the close
|
|
4413
|
+
* `info` the work channel's onDisconnect passes (transport-dependent shape, e.g.
|
|
4414
|
+
* `{ code, reason, local }`), a thrown Error, or a bare string.
|
|
4415
|
+
* @param {unknown} x diagnostic input (close info, Error, or string)
|
|
4416
|
+
* @returns {string|null} a human-readable reason, or null when nothing useful
|
|
4417
|
+
*/
|
|
4418
|
+
function normalizeAgenticMessage(x) {
|
|
4419
|
+
// Collapse an agentic disconnect/failure detail into the single short string
|
|
4420
|
+
// the marker's `agentic.message` field carries (#99 contract). Accepts the
|
|
4421
|
+
// close `info` the work channel's onDisconnect passes (transport-dependent
|
|
4422
|
+
// shape, e.g. `{ code, reason, local }`), a thrown Error, or a bare string,
|
|
4423
|
+
// and returns a human-readable reason or null when there is nothing useful.
|
|
4424
|
+
if (x == null) return null;
|
|
4425
|
+
if (typeof x === 'string') return x.trim() || null;
|
|
4426
|
+
if (x instanceof Error) return x.message ? String(x.message) : String(x);
|
|
4427
|
+
if (typeof x === 'object') {
|
|
4428
|
+
const reason = typeof x.reason === 'string' ? x.reason.trim() : '';
|
|
4429
|
+
const code = x.code != null && x.code !== '' ? String(x.code) : '';
|
|
4430
|
+
if (reason && code) return `${reason} (code ${code})`;
|
|
4431
|
+
if (reason) return reason;
|
|
4432
|
+
if (code) return `close code ${code}`;
|
|
4433
|
+
if (x.message) return String(x.message);
|
|
4434
|
+
if (x.local === true) return 'closed locally';
|
|
4435
|
+
if (x.local === false) return 'connection dropped';
|
|
4436
|
+
return null;
|
|
4437
|
+
}
|
|
4438
|
+
return String(x);
|
|
4439
|
+
}
|
|
4440
|
+
|
|
4441
|
+
/**
|
|
4442
|
+
* Map a resolved `resolveAgenticTarget` result to the INITIAL agentic-channel
|
|
4443
|
+
* state persisted on the supervisor activity marker (#99). Pure so the marker
|
|
4444
|
+
* producer's state transitions are unit-testable without a live broker/SDK
|
|
4445
|
+
* client — a regression here would leave every supervised worker stuck at
|
|
4446
|
+
* `?`/`starting`, which the reader/renderer tests can't catch. `connected`/
|
|
4447
|
+
* `disconnected` are layered on top of this base by the channel lifecycle
|
|
4448
|
+
* (a `{ ...state, status }` merge). The `ambiguous` status is a hard-stop
|
|
4449
|
+
* handled by the caller (never reaches the marker), so it degrades to `off`
|
|
4450
|
+
* here. `safeUrl` mirrors the caller's defensive display-URL builder.
|
|
4451
|
+
* @param {{ status?: string, config?: any, message?: string }} target
|
|
4452
|
+
* @param {(u: string) => (string|null)} [safeUrl]
|
|
4453
|
+
*/
|
|
4454
|
+
function agenticStateForTarget(target, safeUrl = (u) => u) {
|
|
4455
|
+
switch (target?.status) {
|
|
4456
|
+
case 'connect': {
|
|
4457
|
+
const cfg = target.config || {};
|
|
4458
|
+
return {
|
|
4459
|
+
status: 'connecting',
|
|
4460
|
+
mode: cfg.secure ? 'secure' : 'local',
|
|
4461
|
+
url: safeUrl(cfg.url),
|
|
4462
|
+
discovered: cfg.discovered || null,
|
|
4463
|
+
};
|
|
4464
|
+
}
|
|
4465
|
+
case 'advisory':
|
|
4466
|
+
// Retain the discovery diagnostic so the supervisor can distinguish a
|
|
4467
|
+
// missing projects API, a timeout, or a non-Nano endpoint (#99).
|
|
4468
|
+
return { status: 'advisory', message: target.message || null };
|
|
4469
|
+
case 'off':
|
|
4470
|
+
default:
|
|
4471
|
+
return { status: 'off' };
|
|
4472
|
+
}
|
|
4473
|
+
}
|
|
4474
|
+
|
|
4475
|
+
/**
|
|
4476
|
+
* Build the supervisor activity-marker payload the worker atomically writes for
|
|
4477
|
+
* `supervisor status`. Pure so the producer's field set is unit-testable without
|
|
4478
|
+
* spawning a worker: the reader/renderer tests exercise a hand-written marker and
|
|
4479
|
+
* `agenticStateForTarget` in isolation, so a regression that dropped `engine` or
|
|
4480
|
+
* `agentic` from THIS payload — leaving every supervised worker's Engine/Agentic
|
|
4481
|
+
* column stuck at `?` — would otherwise slip through. `jobs` is the live active-job
|
|
4482
|
+
* list; `busy` is derived so callers can't desync it from `jobs`.
|
|
4483
|
+
* @param {{ pid:number, updatedAt:number, jobs:Array<{key:string,type:string,since:number}>, engine:(string|null), agentic:object }} fields
|
|
4484
|
+
*/
|
|
4485
|
+
function buildActivityPayload({ pid, updatedAt, jobs, engine, agentic }) {
|
|
4486
|
+
const jobList = Array.isArray(jobs) ? jobs : [];
|
|
4487
|
+
return {
|
|
4488
|
+
pid,
|
|
4489
|
+
updatedAt,
|
|
4490
|
+
busy: jobList.length > 0,
|
|
4491
|
+
jobs: jobList,
|
|
4492
|
+
engine: engine ?? null,
|
|
4493
|
+
agentic,
|
|
4494
|
+
};
|
|
4495
|
+
}
|
|
4496
|
+
|
|
4410
4497
|
/**
|
|
4411
4498
|
* work — turn a hire profile into live Nano job workers (one per job-type in
|
|
4412
4499
|
* the rank×capability matrix) and poll for work in the foreground until Ctrl-C.
|
|
@@ -4695,10 +4782,29 @@ async function workAgent(req, flags) {
|
|
|
4695
4782
|
installParentDeathWatchdog({ parentPid: Number.isInteger(daemonPid) ? daemonPid : undefined });
|
|
4696
4783
|
}
|
|
4697
4784
|
const activeJobs = new Map(); // jobKey -> { type, since (ms epoch) }
|
|
4785
|
+
// Which engine this worker polls jobs from + the live agentic-visibility
|
|
4786
|
+
// channel status, both surfaced to `supervisor status` via the activity
|
|
4787
|
+
// marker (#99). `agenticState` starts 'starting' and is updated once the
|
|
4788
|
+
// channel target is resolved and again on each connect/disconnect below.
|
|
4789
|
+
// The engine must name the ACTUAL polling authority: jobs are activated by
|
|
4790
|
+
// `camunda.createJobWorker()` against the active c8ctl profile engine
|
|
4791
|
+
// (`camunda.getConfig().restAddress`), whereas `restConfig` can honor the
|
|
4792
|
+
// auxiliary NANO_REST_URL/NANO_BASE_URL/nanoUrl overrides (for `--auto`
|
|
4793
|
+
// reads). Derive from the profile engine, using restConfig only as a
|
|
4794
|
+
// fallback, so the column can't advertise an override host jobs aren't
|
|
4795
|
+
// polled from.
|
|
4796
|
+
const workerEngine = (() => {
|
|
4797
|
+
try {
|
|
4798
|
+
const profileBase = normalizeRestBase(camunda?.getConfig?.()?.restAddress);
|
|
4799
|
+
if (profileBase) return profileBase;
|
|
4800
|
+
} catch { /* degrade to the auxiliary REST config below */ }
|
|
4801
|
+
return restConfig?.baseUrl || null;
|
|
4802
|
+
})();
|
|
4803
|
+
let agenticState = { status: 'starting' };
|
|
4698
4804
|
const writeActivity = () => {
|
|
4699
4805
|
if (!activityFile) return;
|
|
4700
4806
|
const jobs = [...activeJobs.entries()].map(([key, v]) => ({ key, type: v.type, since: v.since }));
|
|
4701
|
-
const payload = { pid: process.pid, updatedAt: Date.now(),
|
|
4807
|
+
const payload = buildActivityPayload({ pid: process.pid, updatedAt: Date.now(), jobs, engine: workerEngine, agentic: agenticState });
|
|
4702
4808
|
const tmp = `${activityFile}.${process.pid}.tmp`;
|
|
4703
4809
|
try {
|
|
4704
4810
|
mkdirSync(dirname(activityFile), { recursive: true });
|
|
@@ -4747,9 +4853,22 @@ async function workAgent(req, flags) {
|
|
|
4747
4853
|
// NANO_AGENTIC=off disables it (see resolveAgenticConfig).
|
|
4748
4854
|
const agenticTarget = await resolveAgenticTarget({ logger });
|
|
4749
4855
|
let agenticCfg = null;
|
|
4856
|
+
// buildAgenticUrl can throw on a malformed/unsupported explicit NANO_AGENTIC_URL.
|
|
4857
|
+
// This is only the display URL for the activity marker, so compute it
|
|
4858
|
+
// defensively: a bad URL must be recorded as a channel failure (via the
|
|
4859
|
+
// createWorkChannel try/catch below), never crash the worker before it — which
|
|
4860
|
+
// would violate the best-effort channel contract and cause a restart loop.
|
|
4861
|
+
const safeAgenticDisplayUrl = (u) => {
|
|
4862
|
+
try { return redactAgenticUrl(buildAgenticUrl(u, {})); }
|
|
4863
|
+
catch { return null; }
|
|
4864
|
+
};
|
|
4750
4865
|
switch (agenticTarget.status) {
|
|
4751
4866
|
case 'connect':
|
|
4752
4867
|
agenticCfg = agenticTarget.config;
|
|
4868
|
+
// 'connecting' until the socket actually opens (wired on the channel
|
|
4869
|
+
// lifecycle below). Carry the resolved mode/target/discovery so the
|
|
4870
|
+
// supervisor can show WHERE presence is being announced (#99).
|
|
4871
|
+
agenticState = agenticStateForTarget(agenticTarget, safeAgenticDisplayUrl);
|
|
4753
4872
|
break;
|
|
4754
4873
|
case 'ambiguous':
|
|
4755
4874
|
// The operator ran with visibility on-by-default but the hub is
|
|
@@ -4758,13 +4877,19 @@ async function workAgent(req, flags) {
|
|
|
4758
4877
|
process.exit(1);
|
|
4759
4878
|
break;
|
|
4760
4879
|
case 'advisory':
|
|
4880
|
+
agenticState = agenticStateForTarget(agenticTarget);
|
|
4761
4881
|
logger.info(` agentic channel: ${agenticTarget.message}`);
|
|
4762
4882
|
break;
|
|
4763
4883
|
case 'off':
|
|
4764
4884
|
default:
|
|
4885
|
+
agenticState = agenticStateForTarget(agenticTarget);
|
|
4765
4886
|
logger.info(' agentic channel: disabled — the off-switch is set (NANO_AGENTIC=off or persisted agentic:false). Clear it to use default LOCAL visibility.');
|
|
4766
4887
|
break;
|
|
4767
4888
|
}
|
|
4889
|
+
// Persist the resolved channel state to the activity marker now, so
|
|
4890
|
+
// `supervisor status` reflects connecting/advisory/off immediately, before
|
|
4891
|
+
// the socket opens (or without a channel at all).
|
|
4892
|
+
writeActivity();
|
|
4768
4893
|
if (agenticCfg) {
|
|
4769
4894
|
try {
|
|
4770
4895
|
workChannel = await createWorkChannel({
|
|
@@ -4789,9 +4914,31 @@ async function workAgent(req, flags) {
|
|
|
4789
4914
|
logger.info(` agentic channel: auto-discovered ${d.project} on the app's /agentic port ${wsHostPart(d.host)}:${d.port} (bypassing the WS-incapable console proxy).`);
|
|
4790
4915
|
}
|
|
4791
4916
|
logger.info(` agentic channel (${mode}): announcing presence as ${workerName} on ${shown}`);
|
|
4917
|
+
// Track the live connection state on the activity marker so the
|
|
4918
|
+
// supervisor shows connected↔disconnected transitions (#99). onConnect
|
|
4919
|
+
// fires only for listeners present at first open, so also reconcile the
|
|
4920
|
+
// already-open case synchronously via connected(). If the socket opened
|
|
4921
|
+
// and then dropped inside the createWorkChannel() await window (before
|
|
4922
|
+
// these listeners existed), connected() is false but everConnected() is
|
|
4923
|
+
// true — record that as `disconnected` rather than leaving it stuck at
|
|
4924
|
+
// `connecting`. A close carries a normalized diagnostic under the contract
|
|
4925
|
+
// `agentic.message` field (not `reason`) so a hub drop explains WHY; a
|
|
4926
|
+
// fresh (re)connect clears any stale message.
|
|
4927
|
+
const markAgentic = (status, message = null) => { agenticState = { ...agenticState, status, message }; writeActivity(); };
|
|
4928
|
+
workChannel.onConnect(() => markAgentic('connected'));
|
|
4929
|
+
workChannel.onReconnect(() => markAgentic('connected'));
|
|
4930
|
+
workChannel.onDisconnect((info) => markAgentic('disconnected', normalizeAgenticMessage(info)));
|
|
4931
|
+
if (workChannel.connected()) markAgentic('connected');
|
|
4932
|
+
else if (workChannel.everConnected()) markAgentic('disconnected');
|
|
4792
4933
|
} catch (err) {
|
|
4793
4934
|
// Never let a channel failure stop the worker from doing its actual job.
|
|
4794
4935
|
workChannel = null;
|
|
4936
|
+
// Retain the failure reason on the marker so the supervisor can show WHY
|
|
4937
|
+
// presence dropped (bad URL, refused socket, …), not just `disconnected`.
|
|
4938
|
+
// The contract diagnostic field is `agentic.message` (#99), matching the
|
|
4939
|
+
// live-disconnect path above — keep the key consistent, not `reason`.
|
|
4940
|
+
agenticState = { ...agenticState, status: 'disconnected', message: normalizeAgenticMessage(err) };
|
|
4941
|
+
writeActivity();
|
|
4795
4942
|
logger.warn(` agentic channel unavailable (${err?.message || err}); continuing without visibility.`);
|
|
4796
4943
|
}
|
|
4797
4944
|
// C4 (#43): observe the client's built-in outbound buffer across the
|
|
@@ -5656,6 +5803,8 @@ function summarizeSupervisorWorker(w, now = Date.now()) {
|
|
|
5656
5803
|
// Per-job activity (supervised workers only). Guard on pid so a stale marker
|
|
5657
5804
|
// left by a previous incarnation can't show a dead job as in-flight.
|
|
5658
5805
|
let activity = null; // { state: 'busy'|'idle', jobs: [{ key, type, sinceMs, sinceEpochMs }] }
|
|
5806
|
+
let engine = null; // job-polling engine base URL this worker reported (#99)
|
|
5807
|
+
let agentic = null; // { status, mode, url, discovered } agentic-channel state (#99)
|
|
5659
5808
|
if (alive) {
|
|
5660
5809
|
const act = readWorkerActivity(w.id);
|
|
5661
5810
|
if (act && act.pid === w.pid) {
|
|
@@ -5670,6 +5819,10 @@ function summarizeSupervisorWorker(w, now = Date.now()) {
|
|
|
5670
5819
|
}))
|
|
5671
5820
|
: [];
|
|
5672
5821
|
activity = { state: jobs.length > 0 ? 'busy' : 'idle', jobs };
|
|
5822
|
+
// Engine + agentic-channel status ride the same pid-guarded marker, so a
|
|
5823
|
+
// stale incarnation can't show a dead worker as connected to a hub.
|
|
5824
|
+
engine = typeof act.engine === 'string' && act.engine ? act.engine : null;
|
|
5825
|
+
agentic = act.agentic && typeof act.agentic === 'object' ? act.agentic : null;
|
|
5673
5826
|
}
|
|
5674
5827
|
// No marker (or a stale-pid one): leave activity null → rendered as unknown.
|
|
5675
5828
|
}
|
|
@@ -5684,6 +5837,8 @@ function summarizeSupervisorWorker(w, now = Date.now()) {
|
|
|
5684
5837
|
lastExit: w.lastExit ?? null,
|
|
5685
5838
|
args: Array.isArray(w.args) ? w.args : [],
|
|
5686
5839
|
activity,
|
|
5840
|
+
engine,
|
|
5841
|
+
agentic,
|
|
5687
5842
|
};
|
|
5688
5843
|
}
|
|
5689
5844
|
|
|
@@ -5710,6 +5865,10 @@ function supervisorStatusSignature(workers) {
|
|
|
5710
5865
|
w.activity
|
|
5711
5866
|
? w.activity.jobs.map((j) => `${j.key}\u0000${j.type ?? ''}`).sort()
|
|
5712
5867
|
: null,
|
|
5868
|
+
// Engine + agentic-channel status: a connect/disconnect or an engine
|
|
5869
|
+
// change is a real transition that must repaint attached consoles (#99).
|
|
5870
|
+
w.engine ?? '',
|
|
5871
|
+
w.agentic ? (w.agentic.status ?? '') : null,
|
|
5713
5872
|
]),
|
|
5714
5873
|
);
|
|
5715
5874
|
}
|
|
@@ -5726,6 +5885,37 @@ function supervisorJobCell(w) {
|
|
|
5726
5885
|
return `${first.key}${more}${dur}`;
|
|
5727
5886
|
}
|
|
5728
5887
|
|
|
5888
|
+
/**
|
|
5889
|
+
* ENGINE cell: the authority (host:port) of the engine this worker polls jobs
|
|
5890
|
+
* from, so an operator can see cross-machine fleets at a glance. `-` for a
|
|
5891
|
+
* down/stopping worker, `?` for a live worker not (yet) reporting or on an
|
|
5892
|
+
* older build whose marker predates this field. A non-URL engine string falls
|
|
5893
|
+
* back to the raw value.
|
|
5894
|
+
*/
|
|
5895
|
+
function supervisorEngineCell(w) {
|
|
5896
|
+
if (w.state !== 'running') return '-';
|
|
5897
|
+
if (!w.activity) return '?'; // alive but not reporting
|
|
5898
|
+
if (!w.engine) return '?'; // reporting, but marker predates the engine field
|
|
5899
|
+
try {
|
|
5900
|
+
const host = new URL(w.engine).host;
|
|
5901
|
+
return host || String(w.engine); // a scheme-less string parses host-empty
|
|
5902
|
+
} catch { return String(w.engine); }
|
|
5903
|
+
}
|
|
5904
|
+
|
|
5905
|
+
/**
|
|
5906
|
+
* AGENTIC cell: the visibility-channel status word
|
|
5907
|
+
* (`connected`/`connecting`/`disconnected`/`advisory`/`off`/`starting`), so an
|
|
5908
|
+
* operator can tell whether presence actually reached the Workforce hub. `-`
|
|
5909
|
+
* for a down/stopping worker, `?` for a live worker not (yet) reporting or on an
|
|
5910
|
+
* older build whose marker predates this field.
|
|
5911
|
+
*/
|
|
5912
|
+
function supervisorAgenticCell(w) {
|
|
5913
|
+
if (w.state !== 'running') return '-';
|
|
5914
|
+
if (!w.activity) return '?'; // alive but not reporting
|
|
5915
|
+
if (!w.agentic || !w.agentic.status) return '?'; // marker predates the agentic field
|
|
5916
|
+
return String(w.agentic.status);
|
|
5917
|
+
}
|
|
5918
|
+
|
|
5729
5919
|
/**
|
|
5730
5920
|
* Re-age a supervisor status snapshot to `now`, recomputing the ticking
|
|
5731
5921
|
* durations (`uptimeMs`, per-job `sinceMs`) from the absolute base epochs the
|
|
@@ -5871,14 +6061,19 @@ function formatSupervisorStatus(status) {
|
|
|
5871
6061
|
id: String(w.id),
|
|
5872
6062
|
profile: String(w.profile),
|
|
5873
6063
|
state: String(w.state),
|
|
6064
|
+
engine: supervisorEngineCell(w),
|
|
6065
|
+
agentic: supervisorAgenticCell(w),
|
|
5874
6066
|
job: supervisorJobCell(w),
|
|
5875
6067
|
pid: w.pid ? String(w.pid) : '-',
|
|
5876
6068
|
restarts: String(w.restarts),
|
|
5877
6069
|
uptime: w.state === 'running' ? formatDuration(w.uptimeMs) : '-',
|
|
5878
6070
|
last: w.lastExit ? String(w.lastExit) : '-',
|
|
5879
6071
|
}));
|
|
5880
|
-
|
|
5881
|
-
|
|
6072
|
+
// ENGINE + AGENTIC sit early (just after STATE) so the pinned live view's
|
|
6073
|
+
// width clamp (which trims from the right) drops the least-critical columns
|
|
6074
|
+
// (LAST EXIT, UPTIME) first and keeps the visibility diagnostics visible.
|
|
6075
|
+
const head = { id: 'ID', profile: 'PROFILE', state: 'STATE', engine: 'ENGINE', agentic: 'AGENTIC', job: 'JOB', pid: 'PID', restarts: 'RESTARTS', uptime: 'UPTIME', last: 'LAST EXIT' };
|
|
6076
|
+
const cols = ['id', 'profile', 'state', 'engine', 'agentic', 'job', 'pid', 'restarts', 'uptime', 'last'];
|
|
5882
6077
|
const width = {};
|
|
5883
6078
|
for (const c of cols) width[c] = Math.max(head[c].length, ...rows.map((r) => r[c].length));
|
|
5884
6079
|
const fmt = (r) => ' ' + cols.map((c) => r[c].padEnd(width[c])).join(' ');
|
|
@@ -8723,6 +8918,11 @@ export {
|
|
|
8723
8918
|
printSupervisorStatus,
|
|
8724
8919
|
supervisorStatusSignature,
|
|
8725
8920
|
supervisorJobCell,
|
|
8921
|
+
supervisorEngineCell,
|
|
8922
|
+
supervisorAgenticCell,
|
|
8923
|
+
agenticStateForTarget,
|
|
8924
|
+
normalizeAgenticMessage,
|
|
8925
|
+
buildActivityPayload,
|
|
8726
8926
|
supervisorWorkerActivityFile,
|
|
8727
8927
|
WORK_FORWARD_FLAGS,
|
|
8728
8928
|
installParentDeathWatchdog,
|
|
@@ -8783,7 +8983,7 @@ export const metadata = {
|
|
|
8783
8983
|
{ command: 'NANO_AGENTIC_URL=http://localhost:8080 NANO_AGENTIC_SECRET=<shared-secret> c8ctl nano work reviewer', description: 'Enrol the worker on the app\'s same-port /agentic channel in SECURE mode (same NANO_AGENTIC_SECRET as the server) so it appears live (presence + relay terminals) on the Workforce visibility page' },
|
|
8784
8984
|
{ command: 'c8ctl nano supervisor start --worker reviewer --worker coder', description: 'Start a detached supervisor managing several workers from one terminal' },
|
|
8785
8985
|
{ command: 'c8ctl nano supervisor', description: 'Attach an interactive console to the supervisor (detach with Ctrl-D, leaving it running)' },
|
|
8786
|
-
{ command: 'c8ctl nano supervisor status', description: 'List supervised workers (
|
|
8986
|
+
{ command: 'c8ctl nano supervisor status', description: 'List supervised workers (state, ENGINE + AGENTIC visibility diagnostics, serviced job / idle, pid, restarts, uptime) without the console' },
|
|
8787
8987
|
{ command: 'c8ctl nano supervisor add decider --max-parallel 2', description: 'Add a supervised worker (forwarding work flags) to the running supervisor' },
|
|
8788
8988
|
{ command: 'c8ctl nano supervisor add reviewer --instances 3', description: 'Add 3 distinct auto-named instances of a profile in one call' },
|
|
8789
8989
|
{ command: 'c8ctl nano supervisor restart reviewer', description: 'Restart a supervised worker by id or profile' },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c8ctl-plugin-nano",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.37.0",
|
|
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",
|
|
@@ -57,12 +57,12 @@
|
|
|
57
57
|
},
|
|
58
58
|
"optionalDependencies": {
|
|
59
59
|
"node-pty": "^1.0.0",
|
|
60
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.
|
|
61
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.
|
|
62
|
-
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.
|
|
63
|
-
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.
|
|
64
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.
|
|
65
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.
|
|
66
|
-
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.
|
|
60
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.37.0",
|
|
61
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.37.0",
|
|
62
|
+
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.37.0",
|
|
63
|
+
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.37.0",
|
|
64
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.37.0",
|
|
65
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.37.0",
|
|
66
|
+
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.37.0"
|
|
67
67
|
}
|
|
68
68
|
}
|
package/work-channel.mjs
CHANGED
|
@@ -118,6 +118,7 @@ function presenceCapability(capability, jobs) {
|
|
|
118
118
|
* @property {(fn: (info: object) => void) => () => void} onDisconnect subscribe to channel close
|
|
119
119
|
* @property {(fn: () => void) => () => void} onReconnect subscribe to reconnects (every open after the first)
|
|
120
120
|
* @property {() => boolean} connected whether the channel is currently open
|
|
121
|
+
* @property {() => boolean} everConnected whether the channel has ever opened (stays true after a later close)
|
|
121
122
|
* @property {() => number} buffered outbound frames currently buffered awaiting the channel
|
|
122
123
|
* @property {(reason?: string) => Promise<void>} stop deregister + close cleanly
|
|
123
124
|
*/
|
|
@@ -272,6 +273,12 @@ export async function createWorkChannel(opts) {
|
|
|
272
273
|
onReconnect: subscribe(reconnectListeners),
|
|
273
274
|
onDisconnect: subscribe(disconnectListeners),
|
|
274
275
|
connected: () => client.connected,
|
|
276
|
+
// Whether the channel has ever opened (even if it has since closed). Lets a
|
|
277
|
+
// late subscriber tell "still connecting, never opened" (false) apart from
|
|
278
|
+
// "opened then dropped before I subscribed" (true), so an initial close that
|
|
279
|
+
// fires inside the createWorkChannel() await window is reconciled to
|
|
280
|
+
// `disconnected` rather than left stuck at `connecting`.
|
|
281
|
+
everConnected: () => hasConnected,
|
|
275
282
|
buffered: () => client.buffered,
|
|
276
283
|
async stop(reason = 'worker stopped') {
|
|
277
284
|
try {
|