c8ctl-plugin-nano 1.58.0 → 1.59.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-endpoint.mjs +80 -9
- package/c8ctl-plugin.js +56 -103
- package/package.json +8 -8
package/agentic-endpoint.mjs
CHANGED
|
@@ -305,7 +305,10 @@ function buildRawEmitClient({ channelUrl, transportFactory, peerAdvertisement, l
|
|
|
305
305
|
* `transportFactory` and never touches the real client.
|
|
306
306
|
*
|
|
307
307
|
* @param {object} opts
|
|
308
|
-
* @param {string} opts.url the app's HTTP(S) base URL (channel served same-port at
|
|
308
|
+
* @param {string} [opts.url] the app's HTTP(S) base URL (channel served same-port at
|
|
309
|
+
* `/agentic`). OPTIONAL in self-heal mode: when `resolveConfig` is supplied the base
|
|
310
|
+
* URL is not fixed, but a static `url` may still be passed to SEED the initial memo
|
|
311
|
+
* before the first re-discovery lands. Required only when `resolveConfig` is absent.
|
|
309
312
|
* @param {string} [opts.token] ADR 0028 identity token (carried as `?token=`)
|
|
310
313
|
* @param {string} [opts.credential] capability credential (carried as `?capability=`)
|
|
311
314
|
* @param {import('@nanobpm/agentic/protocol').ProtocolAdvertisement} [opts.remoteAdvertisement]
|
|
@@ -314,23 +317,91 @@ function buildRawEmitClient({ channelUrl, transportFactory, peerAdvertisement, l
|
|
|
314
317
|
* @param {(state: 'connected'|'disconnected') => void} [opts.onConnectionState] observer fired
|
|
315
318
|
* when the single host connection opens/drops, so a caller can track its liveness
|
|
316
319
|
* @param {{ warn?: Function, debug?: Function }} [opts.logger]
|
|
317
|
-
* @
|
|
320
|
+
* @param {() => Promise<{url:string, token?:string, credential?:string}|null>} [opts.resolveConfig]
|
|
321
|
+
* OPTIONAL self-heal resolver (jwulf/c8ctl-plugin-nano#133). When present the
|
|
322
|
+
* base URL is NOT fixed: each (re)connect consults a background-refreshed,
|
|
323
|
+
* memoised config, and while none is known the synchronous factory THROWS —
|
|
324
|
+
* which `superviseAgentic` turns into its jittered, ≤30s-capped reconnect
|
|
325
|
+
* retry — and kicks a guarded async re-discovery, so a later retry finds the
|
|
326
|
+
* hub and connects (`advisory → connected` without a restart). A moved hub
|
|
327
|
+
* self-heals the same way. Fail-open: a throwing resolver is swallowed.
|
|
328
|
+
* @returns {Promise<() => import('./supervisor.dist.js').RawEmitClient>} a synchronous
|
|
329
|
+
* `connect` factory. In self-heal (`resolveConfig`) mode it may THROW when the
|
|
330
|
+
* hub is not yet discoverable — by contract that drives `superviseAgentic`'s retry.
|
|
318
331
|
*/
|
|
319
332
|
export async function createRawEmitConnect(opts) {
|
|
320
|
-
const { url, token, credential, remoteAdvertisement, transportFactory, logger, onConnectionState } =
|
|
321
|
-
|
|
322
|
-
|
|
333
|
+
const { url, token, credential, remoteAdvertisement, transportFactory, logger, onConnectionState, resolveConfig } =
|
|
334
|
+
opts || {};
|
|
335
|
+
const hasResolver = typeof resolveConfig === 'function';
|
|
336
|
+
const hasStaticUrl = typeof url === 'string' && url.trim() !== '';
|
|
337
|
+
if (!hasResolver && !hasStaticUrl) {
|
|
338
|
+
throw new Error(
|
|
339
|
+
'createRawEmitConnect requires an agentic channel base `url`, or a `resolveConfig` self-heal resolver to discover one',
|
|
340
|
+
);
|
|
323
341
|
}
|
|
324
342
|
|
|
325
|
-
const channelUrl = buildAgenticUrl(url, { token, credential });
|
|
326
343
|
const factory = transportFactory ?? (await loadAgenticClient()).websocketTransport;
|
|
327
344
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
345
|
+
// Static mode: a fixed, known URL (explicit NANO_AGENTIC_URL or an
|
|
346
|
+
// already-discovered hub). The channel URL is built once; the emit client owns
|
|
347
|
+
// reconnect to it and `superviseAgentic` re-`connect`s to the SAME URL on a
|
|
348
|
+
// permanent close. Unchanged behaviour.
|
|
349
|
+
if (!hasResolver) {
|
|
350
|
+
const channelUrl = buildAgenticUrl(url, { token, credential });
|
|
351
|
+
return () =>
|
|
352
|
+
buildRawEmitClient({
|
|
353
|
+
channelUrl,
|
|
354
|
+
transportFactory: factory,
|
|
355
|
+
peerAdvertisement: remoteAdvertisement,
|
|
356
|
+
logger,
|
|
357
|
+
onConnectionState,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Self-heal mode: re-resolve discovery on each (re)connect via a guarded,
|
|
362
|
+
// background-refreshed memoised config. While unresolved the factory throws so
|
|
363
|
+
// `superviseAgentic`'s ≤30s-capped retry doubles as periodic re-discovery.
|
|
364
|
+
let current = hasStaticUrl ? { url, token, credential } : null;
|
|
365
|
+
let refreshing = false;
|
|
366
|
+
const kickRefresh = () => {
|
|
367
|
+
if (refreshing) return;
|
|
368
|
+
refreshing = true;
|
|
369
|
+
Promise.resolve()
|
|
370
|
+
.then(() => resolveConfig())
|
|
371
|
+
.then((next) => {
|
|
372
|
+
if (next && typeof next.url === 'string' && next.url.trim() !== '') {
|
|
373
|
+
// Preserve the last memoised credentials when a later refresh omits
|
|
374
|
+
// them, falling back to the initial opts only when nothing was ever
|
|
375
|
+
// discovered — so a partial refresh never clears a known-good token.
|
|
376
|
+
current = {
|
|
377
|
+
url: next.url,
|
|
378
|
+
token: next.token ?? current?.token ?? token,
|
|
379
|
+
credential: next.credential ?? current?.credential ?? credential,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
})
|
|
383
|
+
.catch((err) => {
|
|
384
|
+
logger?.debug?.(`agentic re-discovery attempt failed: ${err?.message || err}`);
|
|
385
|
+
})
|
|
386
|
+
.finally(() => {
|
|
387
|
+
refreshing = false;
|
|
388
|
+
});
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
return () => {
|
|
392
|
+
// Kick a background refresh on every (re)connect so a moved hub self-heals on
|
|
393
|
+
// the next attempt; it's a no-op while one is in flight, and only runs on a
|
|
394
|
+
// (re)connect (a steady socket never re-enters this factory).
|
|
395
|
+
kickRefresh();
|
|
396
|
+
if (!current) {
|
|
397
|
+
throw new Error('agentic hub not yet discoverable — retrying via supervised reconnect');
|
|
398
|
+
}
|
|
399
|
+
return buildRawEmitClient({
|
|
400
|
+
channelUrl: buildAgenticUrl(current.url, { token: current.token, credential: current.credential }),
|
|
331
401
|
transportFactory: factory,
|
|
332
402
|
peerAdvertisement: remoteAdvertisement,
|
|
333
403
|
logger,
|
|
334
404
|
onConnectionState,
|
|
335
405
|
});
|
|
406
|
+
};
|
|
336
407
|
}
|
package/c8ctl-plugin.js
CHANGED
|
@@ -6715,94 +6715,6 @@ async function resolveAgenticTarget({ camunda, cache, ...opts } = {}) {
|
|
|
6715
6715
|
};
|
|
6716
6716
|
}
|
|
6717
6717
|
|
|
6718
|
-
/**
|
|
6719
|
-
* A jittered backoff schedule (ms) for background agentic re-discovery (#133-A):
|
|
6720
|
-
* ~2s → 4s → 8s → 16s → 30s (capped), each waited value randomised ±20%, spanning
|
|
6721
|
-
* roughly `ceilingMs` (default ~5 minutes) of total elapsed retry time before the
|
|
6722
|
-
* loop gives up. A worker that lost the cold-start discovery race walks this
|
|
6723
|
-
* schedule to upgrade `advisory → connected` without a restart.
|
|
6724
|
-
* @param {{ base?: number, cap?: number, ceilingMs?: number, rng?: () => number }} [opts]
|
|
6725
|
-
* @returns {number[]} the ordered per-attempt wait durations
|
|
6726
|
-
*/
|
|
6727
|
-
function defaultAgenticRediscoveryDelays({
|
|
6728
|
-
base = 2_000,
|
|
6729
|
-
cap = 30_000,
|
|
6730
|
-
ceilingMs = 5 * 60 * 1_000,
|
|
6731
|
-
rng = Math.random,
|
|
6732
|
-
} = {}) {
|
|
6733
|
-
const delays = [];
|
|
6734
|
-
let d = base;
|
|
6735
|
-
let total = 0;
|
|
6736
|
-
while (total < ceilingMs) {
|
|
6737
|
-
const jitter = Math.round((rng() - 0.5) * 0.4 * d); // ±20%
|
|
6738
|
-
const wait = Math.max(500, d + jitter);
|
|
6739
|
-
delays.push(wait);
|
|
6740
|
-
total += wait;
|
|
6741
|
-
d = Math.min(cap, d * 2);
|
|
6742
|
-
}
|
|
6743
|
-
return delays;
|
|
6744
|
-
}
|
|
6745
|
-
|
|
6746
|
-
/**
|
|
6747
|
-
* Background self-heal loop for agentic discovery (#133-A). After an initial
|
|
6748
|
-
* cold-start miss leaves a worker in `advisory`, re-run `resolveTarget` on a
|
|
6749
|
-
* jittered backoff schedule and, on the FIRST attempt that yields a
|
|
6750
|
-
* `status:'connect'` target, invoke `onConnect(target)` and stop — so the worker
|
|
6751
|
-
* upgrades to `connected` without a restart. Fail-open: an attempt whose
|
|
6752
|
-
* `resolveTarget` throws is swallowed and the loop continues; likewise, if
|
|
6753
|
-
* `onConnect` itself throws (a transient channel-open failure), the loop keeps
|
|
6754
|
-
* re-discovering rather than stopping, so retries proceed until a connect
|
|
6755
|
-
* callback actually succeeds. The loop also stops early whenever
|
|
6756
|
-
* `shouldContinue()` returns false (e.g. a channel already came up, or the worker
|
|
6757
|
-
* is shutting down). Returns the connecting target, or `null` if the schedule was
|
|
6758
|
-
* exhausted / cancelled without a hit. Timers and the resolver are injectable so
|
|
6759
|
-
* this is unit-testable without real waits or sockets.
|
|
6760
|
-
*
|
|
6761
|
-
* @param {{
|
|
6762
|
-
* resolveTarget: () => Promise<{status:string, config?:object}>,
|
|
6763
|
-
* onConnect?: (target: {status:string, config?:object}) => (void|Promise<void>),
|
|
6764
|
-
* delaysMs?: number[],
|
|
6765
|
-
* sleep?: (ms:number) => Promise<void>,
|
|
6766
|
-
* shouldContinue?: () => boolean,
|
|
6767
|
-
* logger?: object,
|
|
6768
|
-
* }} opts
|
|
6769
|
-
* @returns {Promise<object|null>}
|
|
6770
|
-
*/
|
|
6771
|
-
async function rediscoverAgenticUntilConnected({
|
|
6772
|
-
resolveTarget,
|
|
6773
|
-
onConnect,
|
|
6774
|
-
delaysMs = defaultAgenticRediscoveryDelays(),
|
|
6775
|
-
sleep = (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
6776
|
-
shouldContinue = () => true,
|
|
6777
|
-
logger = null,
|
|
6778
|
-
} = {}) {
|
|
6779
|
-
if (typeof resolveTarget !== 'function') return null;
|
|
6780
|
-
for (const delay of delaysMs) {
|
|
6781
|
-
if (!shouldContinue()) return null;
|
|
6782
|
-
try { await sleep(delay); } catch { return null; }
|
|
6783
|
-
if (!shouldContinue()) return null;
|
|
6784
|
-
let target;
|
|
6785
|
-
try {
|
|
6786
|
-
target = await resolveTarget();
|
|
6787
|
-
} catch (err) {
|
|
6788
|
-
logger?.debug?.(`agentic re-discovery attempt failed: ${err?.message || err}`);
|
|
6789
|
-
continue;
|
|
6790
|
-
}
|
|
6791
|
-
if (target && target.status === 'connect') {
|
|
6792
|
-
try {
|
|
6793
|
-
await onConnect?.(target);
|
|
6794
|
-
} catch (err) {
|
|
6795
|
-
// A transient channel-open failure must not prematurely stop self-heal:
|
|
6796
|
-
// keep re-discovering until an onConnect callback actually succeeds.
|
|
6797
|
-
logger?.debug?.(`agentic re-discovery onConnect failed, will retry: ${err?.message || err}`);
|
|
6798
|
-
continue;
|
|
6799
|
-
}
|
|
6800
|
-
return target;
|
|
6801
|
-
}
|
|
6802
|
-
}
|
|
6803
|
-
return null;
|
|
6804
|
-
}
|
|
6805
|
-
|
|
6806
6718
|
// Worker-side liveness watchdog defaults (jwulf/c8ctl-plugin-nano#144). A
|
|
6807
6719
|
// previously-connected agentic channel that has been `disconnected` for longer
|
|
6808
6720
|
// than the stale threshold — because the client lib's own reconnect never
|
|
@@ -7828,8 +7740,24 @@ async function workAgent(req, flags) {
|
|
|
7828
7740
|
// worker joins with the well-known LOCAL token and no credential; SECURE mode
|
|
7829
7741
|
// (NANO_AGENTIC_SECRET) sends a real per-peer shared secret as the identity;
|
|
7830
7742
|
// NANO_AGENTIC=off disables it (see resolveAgenticConfig).
|
|
7831
|
-
const
|
|
7743
|
+
const agenticDiscoveryCache = new Map();
|
|
7744
|
+
const agenticTarget = await resolveAgenticTarget({ camunda, logger, cache: agenticDiscoveryCache });
|
|
7832
7745
|
let agenticCfg = null;
|
|
7746
|
+
let agenticSelfHeal = false;
|
|
7747
|
+
// Self-heal resolver (jwulf/c8ctl-plugin-nano#133): re-run discovery on demand
|
|
7748
|
+
// and hand back a fresh connect config, or null while still undiscoverable. The
|
|
7749
|
+
// endpoint's connect factory calls this in the background so `superviseAgentic`'s
|
|
7750
|
+
// ≤30s-capped reconnect doubles as periodic re-discovery — an `advisory`-at-
|
|
7751
|
+
// startup worker (a cold-start discovery miss during an engine hiccup) upgrades
|
|
7752
|
+
// to `connected` without a restart when the hub reappears. The shared cache lets
|
|
7753
|
+
// a later transient miss reuse the last known-good hub (#133-C).
|
|
7754
|
+
const resolveAgenticConnectConfig = async () => {
|
|
7755
|
+
const t = await resolveAgenticTarget({ camunda, logger, cache: agenticDiscoveryCache });
|
|
7756
|
+
if (t && t.status === 'connect') {
|
|
7757
|
+
return { url: t.config.url, token: t.config.token, credential: t.config.credential };
|
|
7758
|
+
}
|
|
7759
|
+
return null;
|
|
7760
|
+
};
|
|
7833
7761
|
// buildAgenticUrl can throw on a malformed/unsupported explicit NANO_AGENTIC_URL.
|
|
7834
7762
|
// This is only the display URL for the activity marker, so compute it
|
|
7835
7763
|
// defensively: a bad URL must be recorded as a channel failure (via the
|
|
@@ -7853,10 +7781,31 @@ async function workAgent(req, flags) {
|
|
|
7853
7781
|
logger.error(` agentic visibility is ambiguous: ${agenticTarget.message}`);
|
|
7854
7782
|
process.exit(1);
|
|
7855
7783
|
break;
|
|
7856
|
-
case 'advisory':
|
|
7857
|
-
|
|
7858
|
-
|
|
7784
|
+
case 'advisory': {
|
|
7785
|
+
// Don't give up: build a SELF-HEALING endpoint whose connect re-discovers
|
|
7786
|
+
// on each of `superviseAgentic`'s ≤30s-capped reconnects, so a cold-start
|
|
7787
|
+
// discovery miss (e.g. the engine hiccuped just as the worker started)
|
|
7788
|
+
// upgrades to `connected` without a restart (jwulf/c8ctl-plugin-nano#133).
|
|
7789
|
+
agenticSelfHeal = true;
|
|
7790
|
+
// Derive one retry-aware message so the marker's `agentic.message` and the
|
|
7791
|
+
// log line agree: consumers of `supervisor status`/markers must see that the
|
|
7792
|
+
// worker WILL self-heal, not the raw "Continuing without it." advisory text.
|
|
7793
|
+
const retryMessage = agenticTarget.message.replace(
|
|
7794
|
+
'Continuing without it.',
|
|
7795
|
+
'Retrying discovery on each ≤30s reconnect until it self-heals.',
|
|
7796
|
+
);
|
|
7797
|
+
// Derive the auth mode from the resolved agentic config so `supervisor
|
|
7798
|
+
// status`/markers report SECURE (NANO_AGENTIC_SECRET) correctly while the
|
|
7799
|
+
// worker is still reconnecting, instead of hard-coding 'local'.
|
|
7800
|
+
agenticState = {
|
|
7801
|
+
status: 'connecting',
|
|
7802
|
+
mode: resolveAgenticConfig(camunda)?.secure ? 'secure' : 'local',
|
|
7803
|
+
url: null,
|
|
7804
|
+
message: retryMessage,
|
|
7805
|
+
};
|
|
7806
|
+
logger.info(` agentic channel: ${retryMessage}`);
|
|
7859
7807
|
break;
|
|
7808
|
+
}
|
|
7860
7809
|
case 'off':
|
|
7861
7810
|
default:
|
|
7862
7811
|
agenticState = agenticStateForTarget(agenticTarget);
|
|
@@ -7885,17 +7834,23 @@ async function workAgent(req, flags) {
|
|
|
7885
7834
|
// the raw-JS wire (`agentic-endpoint.mjs` → the single agentic import surface)
|
|
7886
7835
|
// with the bundle's Effect adapter; additive negotiation still degrades
|
|
7887
7836
|
// claim/release/steer to a no-op against an older hub. Reconnect + resync (and
|
|
7888
|
-
// teardown-on-interruption) are owned by the runtime's `superviseAgentic
|
|
7889
|
-
//
|
|
7890
|
-
//
|
|
7891
|
-
//
|
|
7892
|
-
//
|
|
7893
|
-
|
|
7837
|
+
// teardown-on-interruption) are owned by the runtime's `superviseAgentic`; the
|
|
7838
|
+
// #133 self-heal is REINSTATED without a parallel loop by handing the `advisory`
|
|
7839
|
+
// case a re-discovering connect factory (`resolveConfig`), so `superviseAgentic`'s
|
|
7840
|
+
// own ≤30s-capped reconnect retry IS the periodic re-discovery (the #144/#147
|
|
7841
|
+
// liveness watchdog remains retired) — the marker's agentic status is driven by
|
|
7842
|
+
// the endpoint's connection observer below. Constructing the endpoint opens NO
|
|
7843
|
+
// socket (the runtime's supervision calls the connect factory), so this stays
|
|
7844
|
+
// cheap and side-effect-free until `run` forks.
|
|
7845
|
+
if (agenticCfg || agenticSelfHeal) {
|
|
7894
7846
|
try {
|
|
7895
7847
|
agenticEndpoint = await createAgenticEndpoint({
|
|
7896
|
-
|
|
7897
|
-
|
|
7898
|
-
|
|
7848
|
+
// Discovered/explicit connect: a fixed URL (static factory). Advisory
|
|
7849
|
+
// self-heal: no URL yet — the resolver supplies it (and heals a moved hub)
|
|
7850
|
+
// on each supervised reconnect.
|
|
7851
|
+
...(agenticCfg
|
|
7852
|
+
? { url: agenticCfg.url, token: agenticCfg.token, credential: agenticCfg.credential }
|
|
7853
|
+
: { resolveConfig: resolveAgenticConnectConfig }),
|
|
7899
7854
|
// Keep the activity marker's agentic status honest across the single
|
|
7900
7855
|
// connection's open/drop transitions (#99) — the direct analogue of the
|
|
7901
7856
|
// retired createWorkChannel onConnect/onDisconnect marker wiring.
|
|
@@ -13744,8 +13699,6 @@ export {
|
|
|
13744
13699
|
resolveProbeCandidates,
|
|
13745
13700
|
raceProbeCandidates,
|
|
13746
13701
|
isLinkLocalAddress,
|
|
13747
|
-
rediscoverAgenticUntilConnected,
|
|
13748
|
-
defaultAgenticRediscoveryDelays,
|
|
13749
13702
|
agenticChannelIsStale,
|
|
13750
13703
|
agenticPresenceIsStale,
|
|
13751
13704
|
jitteredDelay,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c8ctl-plugin-nano",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.59.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",
|
|
@@ -73,12 +73,12 @@
|
|
|
73
73
|
},
|
|
74
74
|
"optionalDependencies": {
|
|
75
75
|
"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.
|
|
76
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.59.0",
|
|
77
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.59.0",
|
|
78
|
+
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.59.0",
|
|
79
|
+
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.59.0",
|
|
80
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.59.0",
|
|
81
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.59.0",
|
|
82
|
+
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.59.0"
|
|
83
83
|
}
|
|
84
84
|
}
|