c8ctl-plugin-nano 1.26.2 → 1.27.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/agentic.mjs +106 -0
- package/c8ctl-plugin.js +83 -3
- package/package.json +14 -9
package/agentic.mjs
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Single import surface for the Nano agentic-visibility plane (ADR 0056 —
|
|
2
|
+
// Magikcraft/nano-bpm#670) inside this plugin.
|
|
3
|
+
//
|
|
4
|
+
// This is slice C0 of the agent-visibility epic (jwulf/c8ctl-plugin-nano#38).
|
|
5
|
+
// The sibling slices — C2 presence (#41), C3 PTY relay (#42) and C4 buffer
|
|
6
|
+
// (#43) — MUST import the wire contract and the worker client through *this*
|
|
7
|
+
// module rather than reaching into `@nanobpm/agentic` subpaths or
|
|
8
|
+
// `@nanobpm/urban-agent-client` directly. One place to import, one place to
|
|
9
|
+
// swap when the upstream packaging changes.
|
|
10
|
+
//
|
|
11
|
+
// Nothing here is re-declared: every frame, lane, family, token, vocab and
|
|
12
|
+
// payload type is CONSUMED from the published `@nanobpm/agentic` package, and
|
|
13
|
+
// the worker-side channel client is CONSUMED from `@nanobpm/urban-agent-client`.
|
|
14
|
+
// The shared conformance corpus (`@nanobpm/agentic/protocol/conformance`) keeps
|
|
15
|
+
// this repo's consumption in lock-step with the hub — see
|
|
16
|
+
// `agentic-conformance.test.mjs`.
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Wire contract — @nanobpm/agentic/protocol (S0, the single source of truth).
|
|
20
|
+
// The codec, routing-token grammar, vocab schema and per-family payload
|
|
21
|
+
// validators. These resolve to the package's compiled `dist`, so they load
|
|
22
|
+
// under stock Node.
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
export {
|
|
25
|
+
// message families
|
|
26
|
+
MESSAGE_FAMILIES,
|
|
27
|
+
FAMILY_CODES,
|
|
28
|
+
familyForCode,
|
|
29
|
+
isMessageFamily,
|
|
30
|
+
// QoS lanes
|
|
31
|
+
QOS_LANES,
|
|
32
|
+
LANE_CODES,
|
|
33
|
+
laneForCode,
|
|
34
|
+
lanePriority,
|
|
35
|
+
isQosLane,
|
|
36
|
+
compareFrameOrder,
|
|
37
|
+
// frame codec
|
|
38
|
+
encodeFrame,
|
|
39
|
+
decodeFrame,
|
|
40
|
+
FrameDecodeError,
|
|
41
|
+
FrameEncodeError,
|
|
42
|
+
FRAME_MAGIC,
|
|
43
|
+
FRAME_VERSION,
|
|
44
|
+
FRAME_HEADER_BYTES,
|
|
45
|
+
MAX_SEQ,
|
|
46
|
+
// routing tokens
|
|
47
|
+
parseToken,
|
|
48
|
+
formatToken,
|
|
49
|
+
isValidToken,
|
|
50
|
+
isSegmentName,
|
|
51
|
+
isSeatLabel,
|
|
52
|
+
TokenParseError,
|
|
53
|
+
// vocab schema
|
|
54
|
+
validateVocabDocument,
|
|
55
|
+
// per-family payload contracts
|
|
56
|
+
validatePayload,
|
|
57
|
+
// language-neutral hex helpers (used to hold the codec to the corpus)
|
|
58
|
+
bytesToHex,
|
|
59
|
+
hexToBytes,
|
|
60
|
+
} from '@nanobpm/agentic/protocol';
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Visibility families. Each is re-exported wholesale so a consumer picks the
|
|
64
|
+
// namespace it needs (channel transport, presence, relay lane, transcript)
|
|
65
|
+
// without re-declaring any of it. Namespaced re-exports keep the surface tidy
|
|
66
|
+
// and avoid symbol collisions between the families.
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
export * as channel from '@nanobpm/agentic/channel';
|
|
69
|
+
export * as presence from '@nanobpm/agentic/presence';
|
|
70
|
+
export * as relay from '@nanobpm/agentic/relay';
|
|
71
|
+
export * as transcript from '@nanobpm/agentic/transcript';
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Worker-side channel client — @nanobpm/urban-agent-client.
|
|
75
|
+
//
|
|
76
|
+
// The client's published `dist/protocol.js` imports the contract from
|
|
77
|
+
// `@nanobpm/agentic/source/protocol` (raw TypeScript) on the assumption the
|
|
78
|
+
// consumer runs under a type-stripping loader. Stock Node — which this repo's
|
|
79
|
+
// `node --test` uses — refuses to strip types under `node_modules`
|
|
80
|
+
// (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING), so a *static* re-export of the
|
|
81
|
+
// client would make this whole surface fail to load. To keep C0's surface
|
|
82
|
+
// loadable everywhere while still routing all client consumption through one
|
|
83
|
+
// swap point, the client is exposed behind a lazy async loader. The slice that
|
|
84
|
+
// actually opens the channel (C2, #41) awaits it from the code path that runs
|
|
85
|
+
// under the appropriate loader/build.
|
|
86
|
+
//
|
|
87
|
+
// @typedef {import('@nanobpm/urban-agent-client')} AgenticClientModule
|
|
88
|
+
/** @type {Promise<AgenticClientModule> | undefined} */
|
|
89
|
+
let clientModulePromise;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Load the published worker-side agentic channel client
|
|
93
|
+
* (`@nanobpm/urban-agent-client`). Memoised so repeated calls share one module
|
|
94
|
+
* instance. Import the client only through this accessor so there is a single
|
|
95
|
+
* place to consume it from across the plugin.
|
|
96
|
+
*
|
|
97
|
+
* @returns {Promise<AgenticClientModule>} the client module namespace, exposing
|
|
98
|
+
* `connectAgenticChannel`, `AgenticClient`, `OutboundRing`, the websocket
|
|
99
|
+
* transport and the re-exported protocol symbols.
|
|
100
|
+
*/
|
|
101
|
+
export function loadAgenticClient() {
|
|
102
|
+
if (clientModulePromise === undefined) {
|
|
103
|
+
clientModulePromise = import('@nanobpm/urban-agent-client');
|
|
104
|
+
}
|
|
105
|
+
return clientModulePromise;
|
|
106
|
+
}
|
package/c8ctl-plugin.js
CHANGED
|
@@ -3902,6 +3902,12 @@ const SUPERVISOR_PROBE_RESPONSE_TIMEOUT_MS = 2_000;
|
|
|
3902
3902
|
// Hard cap on a single connection's inbound buffer, so a misbehaving client
|
|
3903
3903
|
// can't grow the daemon's memory without bound with a newline-free frame.
|
|
3904
3904
|
const SUPERVISOR_MAX_FRAME_BYTES = 1 << 20; // 1 MiB
|
|
3905
|
+
// How often the daemon re-samples worker activity to push a refreshed status to
|
|
3906
|
+
// attached consoles. The push is change-gated (see supervisorStatusSignature),
|
|
3907
|
+
// so a quiet fleet stays silent; only real transitions (idle↔busy, a new job,
|
|
3908
|
+
// restart/exit) reprint the table. `NANO_SUPERVISOR_MONITOR_MS=0` disables the
|
|
3909
|
+
// live refresh (falling back to the attach-time snapshot + lifecycle events).
|
|
3910
|
+
const SUPERVISOR_MONITOR_INTERVAL_MS = 1_000;
|
|
3905
3911
|
|
|
3906
3912
|
// The `nano work` flags forwarded verbatim to each spawned child.
|
|
3907
3913
|
// kind: 'value' → `--flag v`; 'boolean' → `--flag`; 'list' → repeated `--flag v`.
|
|
@@ -4184,6 +4190,33 @@ function summarizeSupervisorWorker(w, now = Date.now()) {
|
|
|
4184
4190
|
};
|
|
4185
4191
|
}
|
|
4186
4192
|
|
|
4193
|
+
/**
|
|
4194
|
+
* A stable fingerprint of the fleet's *observable* state for change detection.
|
|
4195
|
+
* Deliberately excludes ticking durations (uptimeMs, per-job sinceMs) so that a
|
|
4196
|
+
* merely-elapsing clock doesn't count as a change — only real transitions (a
|
|
4197
|
+
* worker going up/down, idle↔busy, picking up/finishing a job, a restart) alter
|
|
4198
|
+
* the signature. The daemon uses this to push a refreshed status to attached
|
|
4199
|
+
* consoles only when something actually changed, keeping a quiet fleet silent.
|
|
4200
|
+
* `workers` is an array of `summarizeSupervisorWorker` results.
|
|
4201
|
+
*/
|
|
4202
|
+
function supervisorStatusSignature(workers) {
|
|
4203
|
+
const list = Array.isArray(workers) ? workers : [];
|
|
4204
|
+
return JSON.stringify(
|
|
4205
|
+
list.map((w) => [
|
|
4206
|
+
w.id,
|
|
4207
|
+
w.profile ?? '',
|
|
4208
|
+
w.state,
|
|
4209
|
+
w.pid ?? 0,
|
|
4210
|
+
Number(w.restarts) || 0,
|
|
4211
|
+
w.lastExit ?? '',
|
|
4212
|
+
w.activity ? w.activity.state : null,
|
|
4213
|
+
w.activity
|
|
4214
|
+
? w.activity.jobs.map((j) => `${j.key}\u0000${j.type ?? ''}`).sort()
|
|
4215
|
+
: null,
|
|
4216
|
+
]),
|
|
4217
|
+
);
|
|
4218
|
+
}
|
|
4219
|
+
|
|
4187
4220
|
/** One-line JOB cell for a status row: the serviced job key, `idle`, or `-`. */
|
|
4188
4221
|
function supervisorJobCell(w) {
|
|
4189
4222
|
if (w.state !== 'running') return '-';
|
|
@@ -4338,6 +4371,10 @@ async function runSupervisorDaemon() {
|
|
|
4338
4371
|
const workers = new Map();
|
|
4339
4372
|
const attachClients = new Set();
|
|
4340
4373
|
let shuttingDown = false;
|
|
4374
|
+
// Live-view monitor: tracks the last-broadcast fleet signature so we push a
|
|
4375
|
+
// refreshed status to attached consoles only on real change (see below).
|
|
4376
|
+
let monitorTimer = null;
|
|
4377
|
+
let lastMonitorSig = null;
|
|
4341
4378
|
|
|
4342
4379
|
// Daemon-wide mutation serialization: `add`/`remove`/`restart` must not
|
|
4343
4380
|
// interleave, or two clients racing the same worker could each spawn an
|
|
@@ -4501,11 +4538,15 @@ async function runSupervisorDaemon() {
|
|
|
4501
4538
|
return [...workers.values()].filter((w) => w.profile === t).map((w) => w.id);
|
|
4502
4539
|
};
|
|
4503
4540
|
|
|
4504
|
-
|
|
4541
|
+
// `pub` lets a caller that has already sampled the fleet (e.g. the monitor
|
|
4542
|
+
// tick, which needs the snapshot to compute its change signature) reuse that
|
|
4543
|
+
// exact snapshot for the frame — so the broadcast payload is guaranteed to
|
|
4544
|
+
// match the signature that decided to send it, with no second re-sample.
|
|
4545
|
+
const statusFrame = (final, pub) => ({
|
|
4505
4546
|
ok: true,
|
|
4506
4547
|
type: 'status',
|
|
4507
4548
|
daemon: { pid: process.pid, startedAt, socket: socketPath, logFile: daemonLogFile },
|
|
4508
|
-
workers: [...workers.values()].map(workerPublic),
|
|
4549
|
+
workers: pub || [...workers.values()].map(workerPublic),
|
|
4509
4550
|
...(final ? { final: true } : {}),
|
|
4510
4551
|
});
|
|
4511
4552
|
|
|
@@ -4515,6 +4556,7 @@ async function runSupervisorDaemon() {
|
|
|
4515
4556
|
// Let any in-flight mutation finish before we snapshot the worker set, so
|
|
4516
4557
|
// an add/restart racing the shutdown can't leave an orphaned child behind.
|
|
4517
4558
|
try { await opQueue; } catch { /* mutation already logged */ }
|
|
4559
|
+
if (monitorTimer) { try { clearInterval(monitorTimer); } catch { /* ignore */ } monitorTimer = null; }
|
|
4518
4560
|
dlog(`received ${signal || 'stop'} — stopping ${workers.size} worker(s)`);
|
|
4519
4561
|
await Promise.all([...workers.keys()].map((id) => stopWorker(id)));
|
|
4520
4562
|
broadcast({ type: 'event', event: 'daemon-stop' });
|
|
@@ -4655,6 +4697,37 @@ async function runSupervisorDaemon() {
|
|
|
4655
4697
|
dlog(`supervisor daemon up (pid ${process.pid}) — control ${socketPath}`);
|
|
4656
4698
|
persist();
|
|
4657
4699
|
|
|
4700
|
+
// Live-view refresh: periodically re-sample worker activity and push a fresh
|
|
4701
|
+
// status to attached consoles, but only when the fleet's observable state
|
|
4702
|
+
// actually changed since the last push (idle↔busy, a new/finished job, a
|
|
4703
|
+
// restart/exit). This keeps an attached `supervisor` console current without
|
|
4704
|
+
// spamming a quiet fleet. The signature always tracks the latest state (even
|
|
4705
|
+
// with no clients attached) so an idle-fleet attach — whose snapshot already
|
|
4706
|
+
// matches the tracked signature — won't provoke a redundant reprint for
|
|
4707
|
+
// everyone on the next tick. (A change that lands in the sub-tick window
|
|
4708
|
+
// *between* a tick and a fresh attach can still yield one extra identical
|
|
4709
|
+
// frame to the newcomer; that reprint is required to inform the already-
|
|
4710
|
+
// attached clients of the change, and is harmless — same content, re-rendered.)
|
|
4711
|
+
// Env-gated: NANO_SUPERVISOR_MONITOR_MS=0 disables; otherwise it's the cadence.
|
|
4712
|
+
const monitorMs = (() => {
|
|
4713
|
+
const raw = process.env.NANO_SUPERVISOR_MONITOR_MS;
|
|
4714
|
+
if (raw == null || raw === '') return SUPERVISOR_MONITOR_INTERVAL_MS;
|
|
4715
|
+
const n = Number(raw);
|
|
4716
|
+
return Number.isFinite(n) && n >= 0 ? Math.floor(n) : SUPERVISOR_MONITOR_INTERVAL_MS;
|
|
4717
|
+
})();
|
|
4718
|
+
if (monitorMs > 0) {
|
|
4719
|
+
lastMonitorSig = supervisorStatusSignature([...workers.values()].map(workerPublic));
|
|
4720
|
+
monitorTimer = setInterval(() => {
|
|
4721
|
+
if (shuttingDown) return;
|
|
4722
|
+
const pub = [...workers.values()].map(workerPublic);
|
|
4723
|
+
const sig = supervisorStatusSignature(pub);
|
|
4724
|
+
const changed = sig !== lastMonitorSig;
|
|
4725
|
+
lastMonitorSig = sig;
|
|
4726
|
+
if (changed && attachClients.size > 0) broadcast(statusFrame(false, pub));
|
|
4727
|
+
}, monitorMs);
|
|
4728
|
+
if (typeof monitorTimer.unref === 'function') monitorTimer.unref();
|
|
4729
|
+
}
|
|
4730
|
+
|
|
4658
4731
|
// Keep the event loop alive indefinitely; the server holds it, but add an
|
|
4659
4732
|
// explicit never-resolving guard so a transient server close can't exit us.
|
|
4660
4733
|
await new Promise(() => {});
|
|
@@ -4986,10 +5059,12 @@ async function attachSupervisorConsole(state) {
|
|
|
4986
5059
|
sock.write(encodeFrame({ op: 'attach' }));
|
|
4987
5060
|
|
|
4988
5061
|
let buf = '';
|
|
5062
|
+
let rl = null;
|
|
4989
5063
|
sock.on('data', (chunk) => {
|
|
4990
5064
|
buf += chunk;
|
|
4991
5065
|
const { frames, rest } = decodeFrames(buf);
|
|
4992
5066
|
buf = rest;
|
|
5067
|
+
if (frames.length === 0) return;
|
|
4993
5068
|
for (const frame of frames) {
|
|
4994
5069
|
if (frame.type === 'status') {
|
|
4995
5070
|
out('');
|
|
@@ -5010,9 +5085,13 @@ async function attachSupervisorConsole(state) {
|
|
|
5010
5085
|
out(`! ${frame.error}`);
|
|
5011
5086
|
}
|
|
5012
5087
|
}
|
|
5088
|
+
// A pushed frame writes straight to stdout, stepping on the readline prompt
|
|
5089
|
+
// and any half-typed command. Re-render the prompt (preserving the input
|
|
5090
|
+
// buffer) so an async live-view refresh doesn't corrupt what the user typed.
|
|
5091
|
+
if (rl) { try { rl.prompt(true); } catch { /* ignore */ } }
|
|
5013
5092
|
});
|
|
5014
5093
|
|
|
5015
|
-
|
|
5094
|
+
rl = createReadline({ input: process.stdin, output: process.stdout, prompt: 'supervisor> ' });
|
|
5016
5095
|
rl.prompt();
|
|
5017
5096
|
|
|
5018
5097
|
await new Promise((resolve) => {
|
|
@@ -6519,6 +6598,7 @@ export {
|
|
|
6519
6598
|
formatDuration,
|
|
6520
6599
|
summarizeSupervisorWorker,
|
|
6521
6600
|
formatSupervisorStatus,
|
|
6601
|
+
supervisorStatusSignature,
|
|
6522
6602
|
supervisorJobCell,
|
|
6523
6603
|
supervisorWorkerActivityFile,
|
|
6524
6604
|
WORK_FORWARD_FLAGS,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c8ctl-plugin-nano",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.27.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",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"files": [
|
|
23
23
|
"c8ctl-plugin.js",
|
|
24
24
|
"platforms.mjs",
|
|
25
|
+
"agentic.mjs",
|
|
25
26
|
"nanobpmn-binary.json",
|
|
26
27
|
"README.md"
|
|
27
28
|
],
|
|
@@ -31,7 +32,7 @@
|
|
|
31
32
|
},
|
|
32
33
|
"license": "MIT",
|
|
33
34
|
"engines": {
|
|
34
|
-
"node": ">=
|
|
35
|
+
"node": ">=22.6"
|
|
35
36
|
},
|
|
36
37
|
"c8ctl": {
|
|
37
38
|
"defaults": {
|
|
@@ -46,13 +47,17 @@
|
|
|
46
47
|
"@semantic-release/github": "^12.0.6",
|
|
47
48
|
"semantic-release": "^25.0.3"
|
|
48
49
|
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@nanobpm/agentic": "^0.1.0",
|
|
52
|
+
"@nanobpm/urban-agent-client": "^0.1.0"
|
|
53
|
+
},
|
|
49
54
|
"optionalDependencies": {
|
|
50
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.
|
|
51
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.
|
|
52
|
-
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.
|
|
53
|
-
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.
|
|
54
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.
|
|
55
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.
|
|
56
|
-
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.
|
|
55
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.27.0",
|
|
56
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.27.0",
|
|
57
|
+
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.27.0",
|
|
58
|
+
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.27.0",
|
|
59
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.27.0",
|
|
60
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.27.0",
|
|
61
|
+
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.27.0"
|
|
57
62
|
}
|
|
58
63
|
}
|