c8ctl-plugin-nano 1.60.2 → 1.61.1
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 +42 -6
- package/agent-instance.mjs +166 -11
- package/c8ctl-plugin.js +1238 -47
- package/package.json +10 -9
- package/work-relay.mjs +73 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c8ctl-plugin-nano",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.61.1",
|
|
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",
|
|
@@ -40,9 +40,10 @@
|
|
|
40
40
|
"lint": "node --check c8ctl-plugin.js",
|
|
41
41
|
"typecheck:supervisor": "tsc -p supervisor/tsconfig.json",
|
|
42
42
|
"build:supervisor": "node supervisor/build.mjs",
|
|
43
|
+
"check:bundle": "node scripts/check-bundle.mjs",
|
|
43
44
|
"vendor:effect": "node scripts/vendor-effect.mjs",
|
|
44
45
|
"test:supervisor": "node --experimental-strip-types --test supervisor/test/*.test.ts",
|
|
45
|
-
"test": "node --check c8ctl-plugin.js && npm run typecheck:supervisor && npm run
|
|
46
|
+
"test": "npm run check:bundle && node --check c8ctl-plugin.js && npm run typecheck:supervisor && npm run test:supervisor && node --test --test-timeout=120000 --test-force-exit",
|
|
46
47
|
"prepublishOnly": "npm run typecheck:supervisor && npm run build:supervisor"
|
|
47
48
|
},
|
|
48
49
|
"license": "MIT",
|
|
@@ -73,12 +74,12 @@
|
|
|
73
74
|
},
|
|
74
75
|
"optionalDependencies": {
|
|
75
76
|
"node-pty": "^1.0.0",
|
|
76
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.
|
|
77
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.
|
|
78
|
-
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.
|
|
79
|
-
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.
|
|
80
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.
|
|
81
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.
|
|
82
|
-
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.
|
|
77
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.61.1",
|
|
78
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.61.1",
|
|
79
|
+
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.61.1",
|
|
80
|
+
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.61.1",
|
|
81
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.61.1",
|
|
82
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.61.1",
|
|
83
|
+
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.61.1"
|
|
83
84
|
}
|
|
84
85
|
}
|
package/work-relay.mjs
CHANGED
|
@@ -143,7 +143,7 @@ export function parseInboundRelayChunk(frame, stream) {
|
|
|
143
143
|
* @property {string} stream the relay stream name (derived from the jobKey)
|
|
144
144
|
* @property {(chunk: string|Uint8Array) => void} relay publish one framed, jobKey-tagged output chunk on the relay lane
|
|
145
145
|
* @property {(write: (chunk: string) => void) => (() => void)} attachSteer wire inbound steer bytes for this stream to `write`; returns a detach fn
|
|
146
|
-
* @property {() => Promise<{ closeEmitted: boolean, drained: boolean, timedOut: boolean }>} close emit the `phase:close` lifecycle event, detach any steer subscription, then drain the outbound buffer (bounded)
|
|
146
|
+
* @property {(reason?: string) => Promise<{ closeEmitted: boolean, drained: boolean, timedOut: boolean, updates?: number, bytes?: number, reason?: string }>} close emit the `phase:close` lifecycle event, detach any steer subscription, then drain the outbound buffer (bounded). The channel-backed session ({@link createRelaySession}) IGNORES `reason`, logs no reason/totals, and resolves only `{ closeEmitted, drained, timedOut }`. The host-connection session ({@link createHostRelaySession}) accepts `reason` (normal / job-killed / error), logs it with the update/byte totals (#229), and additionally resolves with the observability totals `updates`, `bytes`, and the resolved `reason`.
|
|
147
147
|
*/
|
|
148
148
|
|
|
149
149
|
/**
|
|
@@ -339,10 +339,13 @@ export function createRelaySession({
|
|
|
339
339
|
* @param {(text: string) => void} opts.publish stream one transcript chunk over the host connection
|
|
340
340
|
* @param {(onChunk: (chunk: string|Uint8Array) => void) => (() => void)} [opts.subscribeSteer]
|
|
341
341
|
* register a steer-in sink for this instance; returns an unsubscribe fn
|
|
342
|
+
* @param {string} [opts.elementInstanceKey] correlation join key stamped on open/close logs (#229)
|
|
343
|
+
* @param {string} [opts.processInstanceKey] correlation join key stamped on open/close logs (#229)
|
|
344
|
+
* @param {string|(() => string)} [opts.agentInstanceKey] the AgentInstance key (or a lazy getter, since it is minted after the session opens) — joins the relay and engine channels (#229)
|
|
342
345
|
* @param {{ warn?: Function, debug?: Function }} [opts.logger]
|
|
343
346
|
* @returns {RelaySession}
|
|
344
347
|
*/
|
|
345
|
-
export function createHostRelaySession({ instance, jobKey, publish, subscribeSteer, logger } = {}) {
|
|
348
|
+
export function createHostRelaySession({ instance, jobKey, publish, subscribeSteer, logger, elementInstanceKey, processInstanceKey, agentInstanceKey } = {}) {
|
|
346
349
|
if (instance === undefined || instance === null || String(instance) === '') {
|
|
347
350
|
throw new Error('createHostRelaySession requires an instance');
|
|
348
351
|
}
|
|
@@ -360,6 +363,29 @@ export function createHostRelaySession({ instance, jobKey, publish, subscribeSte
|
|
|
360
363
|
// the actual wire id is composed by the emit client at emit time.
|
|
361
364
|
const stream = composeStreamId(String(instance), String(jobKey));
|
|
362
365
|
const log = logger || {};
|
|
366
|
+
// #229: the AgentInstance key is only known AFTER the producer's activate()
|
|
367
|
+
// resolves — which can be after this session is created — so accept it as a
|
|
368
|
+
// getter (or a value) and resolve it lazily when logging open/close.
|
|
369
|
+
const resolveAik = () => {
|
|
370
|
+
try {
|
|
371
|
+
const v = typeof agentInstanceKey === 'function' ? agentInstanceKey() : agentInstanceKey;
|
|
372
|
+
return v != null && String(v) !== '' ? String(v) : '?';
|
|
373
|
+
} catch {
|
|
374
|
+
return '?';
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
// #229 cross-channel correlation for the relay lane: join the relay to the
|
|
378
|
+
// AgentInstance + engine channels (today the relay and engine channels can't be
|
|
379
|
+
// reconciled) and to the job/git channels via jobKey/eik/pik.
|
|
380
|
+
const eik = elementInstanceKey != null ? String(elementInstanceKey) : '?';
|
|
381
|
+
const pik = processInstanceKey != null ? String(processInstanceKey) : '?';
|
|
382
|
+
const corr = () => `stream ${stream} aik ${resolveAik()} eik ${eik} pik ${pik} job ${String(jobKey)}`;
|
|
383
|
+
// #229 relay activity accounting: number of agent-output chunks relayed and their
|
|
384
|
+
// total bytes, plus a one-shot "received first update" marker. A relay that opens
|
|
385
|
+
// but never receives an update (the 59-byte husk signature) is now visible.
|
|
386
|
+
let updateCount = 0;
|
|
387
|
+
let byteCount = 0;
|
|
388
|
+
let firstUpdateLogged = false;
|
|
363
389
|
|
|
364
390
|
const relay = (chunk) => {
|
|
365
391
|
if (chunk == null) return;
|
|
@@ -380,6 +406,31 @@ export function createHostRelaySession({ instance, jobKey, publish, subscribeSte
|
|
|
380
406
|
}
|
|
381
407
|
};
|
|
382
408
|
|
|
409
|
+
// Count + log agent-output activity (never the open/close lifecycle markers,
|
|
410
|
+
// which go through relay() directly). The first update proves the relay is live.
|
|
411
|
+
const relayUpdate = (chunk) => {
|
|
412
|
+
if (chunk == null) return;
|
|
413
|
+
// #229: once close() has emitted RELAY_CLOSE_CHUNK, the close marker must stay
|
|
414
|
+
// terminal and the logged totals stable. A one-shot/ACP capture can resolve on
|
|
415
|
+
// timeout/abort before the child's final close event, after which workAgent
|
|
416
|
+
// closes the relay; a late data event arriving here would otherwise publish
|
|
417
|
+
// after the close marker and bump updateCount/byteCount past the close summary.
|
|
418
|
+
if (closed) return;
|
|
419
|
+
const text = typeof chunk === 'string'
|
|
420
|
+
? chunk
|
|
421
|
+
: Buffer.isBuffer(chunk)
|
|
422
|
+
? chunk.toString('utf8')
|
|
423
|
+
: Buffer.from(chunk).toString('utf8');
|
|
424
|
+
if (text === '') return;
|
|
425
|
+
updateCount += 1;
|
|
426
|
+
byteCount += Buffer.byteLength(text, 'utf8');
|
|
427
|
+
if (!firstUpdateLogged) {
|
|
428
|
+
firstUpdateLogged = true;
|
|
429
|
+
try { log.debug?.(`relay received first update (${corr()})`); } catch { /* swallow */ }
|
|
430
|
+
}
|
|
431
|
+
relay(text);
|
|
432
|
+
};
|
|
433
|
+
|
|
383
434
|
// Each attachSteer call owns its own subscription + detach fn (mirrors
|
|
384
435
|
// createRelaySession), so a second attachSteer can't clobber an earlier one.
|
|
385
436
|
const activeDetaches = new Set();
|
|
@@ -429,19 +480,36 @@ export function createHostRelaySession({ instance, jobKey, publish, subscribeSte
|
|
|
429
480
|
// steer subscription is torn down.
|
|
430
481
|
let closed = false;
|
|
431
482
|
let closedPromise = Promise.resolve({ closeEmitted: false, drained: true, timedOut: false });
|
|
432
|
-
const close = () => {
|
|
483
|
+
const close = (reason = 'normal') => {
|
|
433
484
|
if (closed) return closedPromise;
|
|
434
485
|
closed = true;
|
|
435
486
|
// Emit the closing lifecycle twin of RELAY_OPEN_CHUNK so the app can flush the
|
|
436
487
|
// durable transcript deterministically at completion (nano-workforce#710).
|
|
437
488
|
relay(RELAY_CLOSE_CHUNK);
|
|
438
489
|
for (const detach of [...activeDetaches]) detach();
|
|
439
|
-
|
|
490
|
+
// #229: log the close with total updates, total bytes, and the close reason
|
|
491
|
+
// (normal / job-killed / error) so a relay that opened but never received an
|
|
492
|
+
// update (the 59-byte husk) is unambiguous in the worker log.
|
|
493
|
+
try {
|
|
494
|
+
log.debug?.(`relay closed (${corr()}) — ${updateCount} update(s), ${byteCount} byte(s), reason ${reason || 'normal'}`);
|
|
495
|
+
} catch {
|
|
496
|
+
/* swallow */
|
|
497
|
+
}
|
|
498
|
+
closedPromise = Promise.resolve({ closeEmitted: true, drained: true, timedOut: false, updates: updateCount, bytes: byteCount, reason: reason || 'normal' });
|
|
440
499
|
return closedPromise;
|
|
441
500
|
};
|
|
442
501
|
|
|
443
502
|
// Open the stream the instant the session exists (parity with createRelaySession).
|
|
444
503
|
relay(RELAY_OPEN_CHUNK);
|
|
504
|
+
// #229: log the open with the correlation join keys (stream + AgentInstance key +
|
|
505
|
+
// elementInstanceKey/processInstanceKey) so the relay, engine, job and git
|
|
506
|
+
// channels can finally be reconciled — the AgentInstance key is resolved lazily
|
|
507
|
+
// so a producer that mints slightly later is still captured on close.
|
|
508
|
+
try {
|
|
509
|
+
log.debug?.(`relay opened (${corr()})`);
|
|
510
|
+
} catch {
|
|
511
|
+
/* swallow */
|
|
512
|
+
}
|
|
445
513
|
|
|
446
|
-
return { stream, relay, attachSteer, close };
|
|
514
|
+
return { stream, relay: relayUpdate, attachSteer, close };
|
|
447
515
|
}
|