c8ctl-plugin-nano 1.44.10 → 1.44.11
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 +22 -0
- package/c8ctl-plugin.js +199 -14
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -377,6 +377,28 @@ export NANO_AGENTIC_STALE_MS=60000 # force re-discovery if a drop hasn't rec
|
|
|
377
377
|
export NANO_AGENTIC_WATCHDOG_MS=15000 # how often the watchdog checks channel liveness (default)
|
|
378
378
|
```
|
|
379
379
|
|
|
380
|
+
**Lossy-link hardening (reconnect churn that never re-lands presence).** A
|
|
381
|
+
distinct failure mode shows up on a **lossy/roaming WiFi (or NAT) link**: the drop
|
|
382
|
+
*is* detected (an abnormal closure, code `1006`), the client *does* reconnect and
|
|
383
|
+
re-announce — yet presence never re-lands on the hub, so the worker stays absent
|
|
384
|
+
even though its log shows it re-announcing. The sustained-drop watchdog above does
|
|
385
|
+
not catch this, because each brief reconnect keeps resetting its drop clock. Two
|
|
386
|
+
extra safeguards close the gap:
|
|
387
|
+
|
|
388
|
+
- **Presence-keyed watchdog trigger.** A reconnect only counts as recovered once
|
|
389
|
+
the socket *holds* for a short grace window (so a `1006` blip that immediately
|
|
390
|
+
re-drops does not mask an unrecovered presence). When presence has not been
|
|
391
|
+
confirmed within a threshold — regardless of the reconnect flapping — the
|
|
392
|
+
watchdog forces the same full re-discovery + reopen.
|
|
393
|
+
- **Jittered reconnect backoff.** Reconnect attempts are spread with equal-jitter
|
|
394
|
+
backoff so a fleet dropped on the same link does not reconnect in lockstep and
|
|
395
|
+
re-congest it.
|
|
396
|
+
|
|
397
|
+
```bash
|
|
398
|
+
export NANO_AGENTIC_PRESENCE_STALE_MS=60000 # force re-discovery if presence isn't re-confirmed within 60s (default)
|
|
399
|
+
export NANO_AGENTIC_PRESENCE_GRACE_MS=5000 # how long a reconnect must hold before presence counts as landed (default)
|
|
400
|
+
```
|
|
401
|
+
|
|
380
402
|
**Secure mode (opt-in).** For a deployment where you want the visibility channel
|
|
381
403
|
authenticated (rather than open on the LAN), start the server **and** every worker
|
|
382
404
|
box with the **same** `NANO_AGENTIC_SECRET` — same env-var name, same value on both
|
package/c8ctl-plugin.js
CHANGED
|
@@ -6259,6 +6259,20 @@ async function rediscoverAgenticUntilConnected({
|
|
|
6259
6259
|
// client lib alone. Overridable via NANO_AGENTIC_STALE_MS / NANO_AGENTIC_WATCHDOG_MS.
|
|
6260
6260
|
const DEFAULT_AGENTIC_STALE_MS = 60_000;
|
|
6261
6261
|
const DEFAULT_AGENTIC_WATCHDOG_INTERVAL_MS = 15_000;
|
|
6262
|
+
// #147 presence-keyed staleness. #144's stale trigger keys on a SUSTAINED socket
|
|
6263
|
+
// drop (`disconnectedSince` ages past the threshold). On a lossy WiFi/NAT link
|
|
6264
|
+
// that drops `1006` repeatedly, the client lib reconnects and re-announces, but
|
|
6265
|
+
// each brief reconnect clears the drop clock — so the sustained-drop trigger
|
|
6266
|
+
// never fires even though presence never actually re-lands on the hub (the
|
|
6267
|
+
// worker stays absent from the Workers view). The presence-keyed trigger closes
|
|
6268
|
+
// that gap: a connection only counts as "presence confirmed" once it HOLDS for
|
|
6269
|
+
// the grace window, so a reconnect that immediately re-drops cannot mask an
|
|
6270
|
+
// unrecovered presence. When presence has not been confirmed within the stale
|
|
6271
|
+
// threshold — regardless of transient reconnect flapping — the watchdog forces a
|
|
6272
|
+
// full re-discovery + reopen. Overridable via NANO_AGENTIC_PRESENCE_STALE_MS /
|
|
6273
|
+
// NANO_AGENTIC_PRESENCE_GRACE_MS.
|
|
6274
|
+
const DEFAULT_AGENTIC_PRESENCE_STALE_MS = DEFAULT_AGENTIC_STALE_MS;
|
|
6275
|
+
const DEFAULT_AGENTIC_PRESENCE_GRACE_MS = 5_000;
|
|
6262
6276
|
|
|
6263
6277
|
/**
|
|
6264
6278
|
* Decide whether a worker's agentic channel is *stale* — i.e. it once connected,
|
|
@@ -6293,6 +6307,78 @@ function agenticChannelIsStale({
|
|
|
6293
6307
|
return now() - since >= staleAfterMs;
|
|
6294
6308
|
}
|
|
6295
6309
|
|
|
6310
|
+
/**
|
|
6311
|
+
* Decide whether a worker's agentic channel is *presence-stale* (#147) — i.e. it
|
|
6312
|
+
* once connected but its presence has NOT been re-confirmed on the hub within
|
|
6313
|
+
* `staleAfterMs`, even though the socket may be intermittently reconnecting. This
|
|
6314
|
+
* is the churn counterpart to {@link agenticChannelIsStale}: on a lossy link the
|
|
6315
|
+
* client lib reconnects and re-announces after every `1006`, which keeps clearing
|
|
6316
|
+
* the sustained-drop clock, so the socket-keyed trigger never fires — yet
|
|
6317
|
+
* presence never actually lands. `presenceHealthySince` is the epoch-ms of the
|
|
6318
|
+
* last time the channel was observed *stably* connected long enough for presence
|
|
6319
|
+
* to be considered landed. The first open seeds it immediately (that open drains
|
|
6320
|
+
* the buffered REGISTER, so presence lands with it); thereafter it advances only
|
|
6321
|
+
* once a connection has *held* past a grace window (via `onPresenceHealthy`), so a
|
|
6322
|
+
* reconnect that immediately re-drops does not count. Pure so the trigger is
|
|
6323
|
+
* unit-testable
|
|
6324
|
+
* without timers or sockets. A channel that never opened is not presence-stale
|
|
6325
|
+
* (the initial connect owns it); a null `presenceHealthySince` (never confirmed,
|
|
6326
|
+
* or reset by a heal) also reads as not-stale so a fresh open is given its grace.
|
|
6327
|
+
*
|
|
6328
|
+
* @param {{
|
|
6329
|
+
* everConnected: () => boolean,
|
|
6330
|
+
* presenceHealthySince?: () => (number|null),
|
|
6331
|
+
* now?: () => number,
|
|
6332
|
+
* staleAfterMs?: number,
|
|
6333
|
+
* }} opts
|
|
6334
|
+
* @returns {boolean}
|
|
6335
|
+
*/
|
|
6336
|
+
function agenticPresenceIsStale({
|
|
6337
|
+
everConnected,
|
|
6338
|
+
presenceHealthySince,
|
|
6339
|
+
now = () => Date.now(),
|
|
6340
|
+
staleAfterMs = DEFAULT_AGENTIC_PRESENCE_STALE_MS,
|
|
6341
|
+
}) {
|
|
6342
|
+
if (typeof everConnected !== 'function') return false;
|
|
6343
|
+
if (!everConnected()) return false; // never opened → the initial connect owns it
|
|
6344
|
+
const since = typeof presenceHealthySince === 'function' ? presenceHealthySince() : null;
|
|
6345
|
+
if (since == null) return false; // never confirmed / freshly reset → give the open its grace
|
|
6346
|
+
return now() - since >= staleAfterMs;
|
|
6347
|
+
}
|
|
6348
|
+
|
|
6349
|
+
/**
|
|
6350
|
+
* Equal-jitter backoff (#147). Given a base backoff `baseMs`, keep half of it and
|
|
6351
|
+
* randomise the other half: `baseMs/2 + rand()*baseMs/2`. A fleet of workers that
|
|
6352
|
+
* all dropped on the same lossy link would otherwise reconnect in lockstep (the
|
|
6353
|
+
* client lib's exponential backoff is deterministic), re-congesting the link and
|
|
6354
|
+
* re-triggering the `1006` drops that stranded them. Spreading the reconnect
|
|
6355
|
+
* attempts bounds that thundering-herd churn while preserving the exponential
|
|
6356
|
+
* growth of the underlying policy. Pure (`rand` injectable) so it is testable.
|
|
6357
|
+
*
|
|
6358
|
+
* @param {number} baseMs the deterministic backoff the client lib computed
|
|
6359
|
+
* @param {{ rand?: () => number }} [opts]
|
|
6360
|
+
* @returns {number} the jittered delay in ms (0 when baseMs is non-positive)
|
|
6361
|
+
*/
|
|
6362
|
+
function jitteredDelay(baseMs, { rand = Math.random } = {}) {
|
|
6363
|
+
const b = Number.isFinite(baseMs) && baseMs > 0 ? baseMs : 0;
|
|
6364
|
+
if (b === 0) return 0;
|
|
6365
|
+
const half = b / 2;
|
|
6366
|
+
return Math.round(half + rand() * half);
|
|
6367
|
+
}
|
|
6368
|
+
|
|
6369
|
+
/**
|
|
6370
|
+
* Build a reconnect `schedule` function that applies {@link jitteredDelay} to the
|
|
6371
|
+
* backoff the client lib passes, so the worker's reconnect attempts on a lossy
|
|
6372
|
+
* link are de-synchronised (#147). Drops straight into `createWorkChannel`'s
|
|
6373
|
+
* injectable `schedule` seam; the timer is injectable for tests.
|
|
6374
|
+
*
|
|
6375
|
+
* @param {{ rand?: () => number, timer?: (fn: () => void, ms: number) => any }} [opts]
|
|
6376
|
+
* @returns {(fn: () => void, ms: number) => void}
|
|
6377
|
+
*/
|
|
6378
|
+
function makeJitteredReconnectSchedule({ rand = Math.random, timer = setTimeout } = {}) {
|
|
6379
|
+
return (fn, ms) => { timer(fn, jitteredDelay(ms, { rand })); };
|
|
6380
|
+
}
|
|
6381
|
+
|
|
6296
6382
|
/**
|
|
6297
6383
|
* Start the worker-side agentic-channel liveness watchdog (#144). On a fixed
|
|
6298
6384
|
* interval it asks {@link agenticChannelIsStale} whether the channel dropped and
|
|
@@ -6310,9 +6396,23 @@ function agenticChannelIsStale({
|
|
|
6310
6396
|
* prevent a stale-channel resurrection mid-teardown), and `tick()` runs a single
|
|
6311
6397
|
* check (tests drive it directly).
|
|
6312
6398
|
*
|
|
6399
|
+
* #147 adds a second, presence-keyed trigger alongside the #144 sustained-drop
|
|
6400
|
+
* one: each tick, a channel observed *stably* connected (connected for at least
|
|
6401
|
+
* `presenceGraceMs` since `connectedSince()`) advances the presence-health clock
|
|
6402
|
+
* via `onPresenceHealthy()`; when presence has not been confirmed within
|
|
6403
|
+
* `presenceStaleAfterMs` — even while the socket flaps `1006` reconnects — the
|
|
6404
|
+
* watchdog heals just as it does for a sustained drop. The presence accessors are
|
|
6405
|
+
* optional: omitting them leaves the #147 trigger inert, so the #144 behaviour is
|
|
6406
|
+
* unchanged.
|
|
6407
|
+
*
|
|
6313
6408
|
* @param {{
|
|
6314
6409
|
* getChannel: () => (import('./work-channel.mjs').WorkChannel | null),
|
|
6315
6410
|
* disconnectedSince: () => (number|null),
|
|
6411
|
+
* connectedSince?: () => (number|null),
|
|
6412
|
+
* presenceHealthySince?: () => (number|null),
|
|
6413
|
+
* onPresenceHealthy?: () => void,
|
|
6414
|
+
* presenceGraceMs?: number,
|
|
6415
|
+
* presenceStaleAfterMs?: number,
|
|
6316
6416
|
* onStale: () => (void|Promise<void>),
|
|
6317
6417
|
* staleAfterMs?: number,
|
|
6318
6418
|
* intervalMs?: number,
|
|
@@ -6327,6 +6427,11 @@ function startAgenticChannelWatchdog({
|
|
|
6327
6427
|
getChannel,
|
|
6328
6428
|
disconnectedSince,
|
|
6329
6429
|
onStale,
|
|
6430
|
+
connectedSince = null,
|
|
6431
|
+
presenceHealthySince = null,
|
|
6432
|
+
onPresenceHealthy = null,
|
|
6433
|
+
presenceGraceMs = DEFAULT_AGENTIC_PRESENCE_GRACE_MS,
|
|
6434
|
+
presenceStaleAfterMs = DEFAULT_AGENTIC_PRESENCE_STALE_MS,
|
|
6330
6435
|
staleAfterMs = DEFAULT_AGENTIC_STALE_MS,
|
|
6331
6436
|
intervalMs = DEFAULT_AGENTIC_WATCHDOG_INTERVAL_MS,
|
|
6332
6437
|
now = () => Date.now(),
|
|
@@ -6347,17 +6452,39 @@ function startAgenticChannelWatchdog({
|
|
|
6347
6452
|
const tick = async () => {
|
|
6348
6453
|
if (stopped || healing) return; // shutting down, or a heal is in flight — don't stack a second re-discovery
|
|
6349
6454
|
const ch = typeof getChannel === 'function' ? getChannel() : null;
|
|
6455
|
+
// #147: advance the presence-health clock when the channel is observed
|
|
6456
|
+
// STABLY connected (held for the grace window). A churny link that reconnects
|
|
6457
|
+
// but immediately re-drops never accrues the grace, so its health clock ages
|
|
6458
|
+
// out and the presence-keyed trigger below fires — unlike the sustained-drop
|
|
6459
|
+
// trigger, which the churn keeps resetting. Inert unless the accessors are wired.
|
|
6460
|
+
if (ch && typeof onPresenceHealthy === 'function' && typeof connectedSince === 'function') {
|
|
6461
|
+
try {
|
|
6462
|
+
const cs = connectedSince();
|
|
6463
|
+
if (ch.connected() && cs != null && now() - cs >= presenceGraceMs) onPresenceHealthy();
|
|
6464
|
+
} catch { /* best effort — never let a health probe break the tick */ }
|
|
6465
|
+
}
|
|
6466
|
+
// The channel is stale when EITHER trigger fires: #144's sustained socket
|
|
6467
|
+
// drop, or #147's presence-not-confirmed-despite-reconnect-churn.
|
|
6468
|
+
const stale = !!ch && (
|
|
6469
|
+
agenticChannelIsStale({
|
|
6470
|
+
connected: () => ch.connected(),
|
|
6471
|
+
everConnected: () => ch.everConnected(),
|
|
6472
|
+
disconnectedSince,
|
|
6473
|
+
now,
|
|
6474
|
+
staleAfterMs,
|
|
6475
|
+
})
|
|
6476
|
+
|| agenticPresenceIsStale({
|
|
6477
|
+
everConnected: () => ch.everConnected(),
|
|
6478
|
+
presenceHealthySince,
|
|
6479
|
+
now,
|
|
6480
|
+
staleAfterMs: presenceStaleAfterMs,
|
|
6481
|
+
})
|
|
6482
|
+
);
|
|
6350
6483
|
// No channel object → the initial open or the cold-start self-heal loop owns
|
|
6351
6484
|
// recovery; the watchdog only guards a channel that HAS connected and stalled.
|
|
6352
6485
|
// A missing or non-stale (healthy / recovered / still-connecting) channel also
|
|
6353
6486
|
// ends any current stale episode, so re-arm the latch for the next one.
|
|
6354
|
-
if (!
|
|
6355
|
-
connected: () => ch.connected(),
|
|
6356
|
-
everConnected: () => ch.everConnected(),
|
|
6357
|
-
disconnectedSince,
|
|
6358
|
-
now,
|
|
6359
|
-
staleAfterMs,
|
|
6360
|
-
})) {
|
|
6487
|
+
if (!stale) {
|
|
6361
6488
|
firedForEpisode = false;
|
|
6362
6489
|
return;
|
|
6363
6490
|
}
|
|
@@ -6366,9 +6493,16 @@ function startAgenticChannelWatchdog({
|
|
|
6366
6493
|
firedForEpisode = true;
|
|
6367
6494
|
try {
|
|
6368
6495
|
if (stopped) return; // shutdown raced us between the checks — do not heal
|
|
6369
|
-
const since = disconnectedSince();
|
|
6370
|
-
|
|
6371
|
-
|
|
6496
|
+
const since = typeof disconnectedSince === 'function' ? disconnectedSince() : null;
|
|
6497
|
+
// During reconnect churn `disconnectedSince` can be null (no sustained drop)
|
|
6498
|
+
// or reset by the latest blip, so it under-reports the real staleness. Fall
|
|
6499
|
+
// back to the presence-health clock — the signal we actually acted on — so
|
|
6500
|
+
// the logged age reflects how long presence has genuinely been unconfirmed.
|
|
6501
|
+
const staleSince = since != null
|
|
6502
|
+
? since
|
|
6503
|
+
: (typeof presenceHealthySince === 'function' ? presenceHealthySince() : null);
|
|
6504
|
+
const downFor = staleSince != null ? Math.round((now() - staleSince) / 1000) : '?';
|
|
6505
|
+
logger?.warn?.(` agentic channel: presence not confirmed within threshold (down ${downFor}s / reconnect churn) — forcing re-discovery (the client lib did not self-heal; likely a lossy-link 1006 churn or half-open drop).`);
|
|
6372
6506
|
await onStale?.();
|
|
6373
6507
|
} catch (err) {
|
|
6374
6508
|
firedForEpisode = false; // heal failed → re-arm so a later tick retries this episode
|
|
@@ -6888,6 +7022,16 @@ async function workAgent(req, flags) {
|
|
|
6888
7022
|
// shutdown); `agenticSelfHealing` guards against two concurrent re-discovery
|
|
6889
7023
|
// loops (the cold-start one and a watchdog-triggered one).
|
|
6890
7024
|
let agenticDisconnectedSince = null;
|
|
7025
|
+
// #147 presence-keyed watchdog state. `agenticConnectedSince` is the epoch-ms
|
|
7026
|
+
// the channel last (re)connected (null while down); the watchdog uses it to
|
|
7027
|
+
// require a stable connection to have held for a grace window before counting
|
|
7028
|
+
// presence as confirmed. `agenticPresenceHealthyAt` is the epoch-ms presence
|
|
7029
|
+
// was last confirmed healthy — advanced on the first connect (buffered REGISTER
|
|
7030
|
+
// drains) and by the watchdog whenever a stable connection is observed, and
|
|
7031
|
+
// reset by a heal. When it ages past the presence-stale threshold — even while
|
|
7032
|
+
// the socket flaps `1006` reconnects — the watchdog forces a re-discovery.
|
|
7033
|
+
let agenticConnectedSince = null;
|
|
7034
|
+
let agenticPresenceHealthyAt = null;
|
|
6891
7035
|
/** @type {{ stop: () => void } | null} */
|
|
6892
7036
|
let agenticWatchdog = null;
|
|
6893
7037
|
let agenticSelfHealing = false;
|
|
@@ -6985,6 +7129,11 @@ async function workAgent(req, flags) {
|
|
|
6985
7129
|
token: cfg.token,
|
|
6986
7130
|
credential: cfg.credential,
|
|
6987
7131
|
bufferCapacity: cfg.bufferCapacity,
|
|
7132
|
+
// #147: de-synchronise reconnect attempts with equal-jitter backoff so a
|
|
7133
|
+
// fleet dropped on the same lossy link does not reconnect in lockstep and
|
|
7134
|
+
// re-congest it. Wraps the client lib's own exponential policy (which has
|
|
7135
|
+
// no jitter of its own); the base delays/factor stay the lib's defaults.
|
|
7136
|
+
schedule: makeJitteredReconnectSchedule(),
|
|
6988
7137
|
logger,
|
|
6989
7138
|
});
|
|
6990
7139
|
const shown = redactAgenticUrl(buildAgenticUrl(cfg.url, {}));
|
|
@@ -7004,16 +7153,38 @@ async function workAgent(req, flags) {
|
|
|
7004
7153
|
// a disconnect starts it (first drop wins, so the watchdog measures from the
|
|
7005
7154
|
// ORIGINAL drop, not the latest of a reconnect storm). The watchdog reads
|
|
7006
7155
|
// this to decide when the client lib has failed to self-heal.
|
|
7007
|
-
workChannel.onConnect(() => {
|
|
7008
|
-
|
|
7156
|
+
workChannel.onConnect(() => {
|
|
7157
|
+
markAgentic('connected');
|
|
7158
|
+
agenticDisconnectedSince = null;
|
|
7159
|
+
agenticConnectedSince = Date.now();
|
|
7160
|
+
// First open drains the buffered REGISTER → presence lands; seed the
|
|
7161
|
+
// presence-health clock so the #147 trigger measures from here (#147).
|
|
7162
|
+
agenticPresenceHealthyAt = Date.now();
|
|
7163
|
+
});
|
|
7164
|
+
workChannel.onReconnect(() => {
|
|
7165
|
+
markAgentic('connected');
|
|
7166
|
+
agenticDisconnectedSince = null;
|
|
7167
|
+
agenticConnectedSince = Date.now();
|
|
7168
|
+
// Deliberately do NOT advance agenticPresenceHealthyAt here: a reconnect
|
|
7169
|
+
// only CLAIMS presence (re-announces). On a lossy link the socket may
|
|
7170
|
+
// re-drop `1006` before presence actually lands, so the watchdog confirms
|
|
7171
|
+
// it only once a connection HOLDS for the grace window — a reconnect that
|
|
7172
|
+
// immediately re-drops must not mask an unrecovered presence (#147).
|
|
7173
|
+
});
|
|
7009
7174
|
workChannel.onDisconnect((info) => {
|
|
7010
7175
|
markAgentic('disconnected', normalizeAgenticMessage(info));
|
|
7011
7176
|
if (agenticDisconnectedSince == null) agenticDisconnectedSince = Date.now();
|
|
7177
|
+
agenticConnectedSince = null;
|
|
7012
7178
|
});
|
|
7013
|
-
if (workChannel.connected()) {
|
|
7014
|
-
|
|
7179
|
+
if (workChannel.connected()) {
|
|
7180
|
+
markAgentic('connected');
|
|
7181
|
+
agenticDisconnectedSince = null;
|
|
7182
|
+
agenticConnectedSince = Date.now();
|
|
7183
|
+
if (agenticPresenceHealthyAt == null) agenticPresenceHealthyAt = Date.now();
|
|
7184
|
+
} else if (workChannel.everConnected()) {
|
|
7015
7185
|
markAgentic('disconnected');
|
|
7016
7186
|
if (agenticDisconnectedSince == null) agenticDisconnectedSince = Date.now();
|
|
7187
|
+
agenticConnectedSince = null;
|
|
7017
7188
|
}
|
|
7018
7189
|
} catch (err) {
|
|
7019
7190
|
// Never let a channel failure stop the worker from doing its actual job.
|
|
@@ -7093,6 +7264,8 @@ async function workAgent(req, flags) {
|
|
|
7093
7264
|
if (!stale) return;
|
|
7094
7265
|
workChannel = null; // re-arms armAgenticSelfHeal()'s shouldContinue gate
|
|
7095
7266
|
agenticDisconnectedSince = null; // reset the clock; the fresh open restarts it
|
|
7267
|
+
agenticConnectedSince = null; // #147: the fresh open re-seeds it
|
|
7268
|
+
agenticPresenceHealthyAt = null; // #147: the fresh open re-confirms presence
|
|
7096
7269
|
try { bufferMonitor?.stop(); } catch { /* best effort */ }
|
|
7097
7270
|
bufferMonitor = null;
|
|
7098
7271
|
markAgentic('disconnected', 'stale channel — re-discovering hub');
|
|
@@ -7106,9 +7279,18 @@ async function workAgent(req, flags) {
|
|
|
7106
7279
|
if (agenticWatchdog) return;
|
|
7107
7280
|
const staleAfterMs = Math.max(5_000, intFlag(process.env.NANO_AGENTIC_STALE_MS, DEFAULT_AGENTIC_STALE_MS));
|
|
7108
7281
|
const intervalMs = Math.max(1_000, intFlag(process.env.NANO_AGENTIC_WATCHDOG_MS, DEFAULT_AGENTIC_WATCHDOG_INTERVAL_MS));
|
|
7282
|
+
const presenceStaleAfterMs = Math.max(5_000, intFlag(process.env.NANO_AGENTIC_PRESENCE_STALE_MS, DEFAULT_AGENTIC_PRESENCE_STALE_MS));
|
|
7283
|
+
const presenceGraceMs = Math.max(1_000, intFlag(process.env.NANO_AGENTIC_PRESENCE_GRACE_MS, DEFAULT_AGENTIC_PRESENCE_GRACE_MS));
|
|
7109
7284
|
agenticWatchdog = startAgenticChannelWatchdog({
|
|
7110
7285
|
getChannel: () => workChannel,
|
|
7111
7286
|
disconnectedSince: () => agenticDisconnectedSince,
|
|
7287
|
+
// #147: presence-keyed trigger — heal reconnect-churn that never re-lands
|
|
7288
|
+
// presence, not just a sustained socket drop.
|
|
7289
|
+
connectedSince: () => agenticConnectedSince,
|
|
7290
|
+
presenceHealthySince: () => agenticPresenceHealthyAt,
|
|
7291
|
+
onPresenceHealthy: () => { agenticPresenceHealthyAt = Date.now(); },
|
|
7292
|
+
presenceStaleAfterMs,
|
|
7293
|
+
presenceGraceMs,
|
|
7112
7294
|
onStale: healStaleAgenticChannel,
|
|
7113
7295
|
staleAfterMs,
|
|
7114
7296
|
intervalMs,
|
|
@@ -12154,6 +12336,9 @@ export {
|
|
|
12154
12336
|
rediscoverAgenticUntilConnected,
|
|
12155
12337
|
defaultAgenticRediscoveryDelays,
|
|
12156
12338
|
agenticChannelIsStale,
|
|
12339
|
+
agenticPresenceIsStale,
|
|
12340
|
+
jitteredDelay,
|
|
12341
|
+
makeJitteredReconnectSchedule,
|
|
12157
12342
|
startAgenticChannelWatchdog,
|
|
12158
12343
|
};
|
|
12159
12344
|
export { compareSemver, githubRepoSlug, filterReleasesSince, renderReleaseBody };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c8ctl-plugin-nano",
|
|
3
|
-
"version": "1.44.
|
|
3
|
+
"version": "1.44.11",
|
|
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.44.
|
|
61
|
-
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.44.
|
|
62
|
-
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.44.
|
|
63
|
-
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.44.
|
|
64
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.44.
|
|
65
|
-
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.44.
|
|
66
|
-
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.44.
|
|
60
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.44.11",
|
|
61
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.44.11",
|
|
62
|
+
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.44.11",
|
|
63
|
+
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.44.11",
|
|
64
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.44.11",
|
|
65
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.44.11",
|
|
66
|
+
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.44.11"
|
|
67
67
|
}
|
|
68
68
|
}
|