c8ctl-plugin-nano 1.44.8 → 1.44.9
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 +17 -1
- package/c8ctl-plugin.js +225 -17
- package/package.json +8 -8
- package/work-channel.mjs +17 -7
package/README.md
CHANGED
|
@@ -352,7 +352,7 @@ one of:
|
|
|
352
352
|
| `starting` | transient: the worker just spawned and hasn't resolved its channel target yet (pre-`connecting`) |
|
|
353
353
|
| `connected` | presence is live on the hub — you should see this worker in the Cockpit |
|
|
354
354
|
| `connecting` | resolved a hub, socket not open yet (or the hub is unreachable) |
|
|
355
|
-
| `disconnected` | an established channel dropped (hub restart/outage) — it auto-reconnects; also set if the channel failed to start (bad URL/refused socket),
|
|
355
|
+
| `disconnected` | an established channel dropped (hub restart/outage) — it auto-reconnects, and a worker-side **liveness watchdog** force-re-discovers the hub if it stays down (see below); also set if the channel failed to start (bad URL/refused socket), which is **not** auto-recovered by the watchdog (it only guards a channel that has connected) — fix the target and restart the worker |
|
|
356
356
|
| `advisory` | nothing discoverable at the engine — **not** in the Cockpit; set `NANO_AGENTIC_URL` |
|
|
357
357
|
| `off` | visibility disabled (`NANO_AGENTIC=off`) |
|
|
358
358
|
| `?` | a live worker not yet reporting, or an older build predating these fields |
|
|
@@ -361,6 +361,22 @@ If workers show `advisory` (or stay `connecting`) while jobs still run, that's t
|
|
|
361
361
|
"connected to the engine but empty Cockpit" case: point them at the app with
|
|
362
362
|
`export NANO_AGENTIC_URL=http://<engine-host>:<appUi.port>` (e.g. `:3000`).
|
|
363
363
|
|
|
364
|
+
**Liveness watchdog (auto-recovery from a wedged channel).** If the nano server
|
|
365
|
+
restarts, crashes, or a network partition drops the connection *without* a clean
|
|
366
|
+
close (a **half-open** socket), a worker's channel client can sit `disconnected`
|
|
367
|
+
forever — the worker vanishes from the Nano Workers view / Cockpit and, before
|
|
368
|
+
this, only a supervisor restart brought it back. Each worker now runs a
|
|
369
|
+
belt-and-suspenders watchdog: once a channel that had connected stays down past a
|
|
370
|
+
threshold (the client library's own reconnect never recovered it), the worker
|
|
371
|
+
tears the wedged channel down and re-runs full hub discovery + reopen — no restart
|
|
372
|
+
needed. The thresholds are tunable via env (sensible defaults; you rarely need
|
|
373
|
+
these):
|
|
374
|
+
|
|
375
|
+
```bash
|
|
376
|
+
export NANO_AGENTIC_STALE_MS=60000 # force re-discovery if a drop hasn't recovered within 60s (default)
|
|
377
|
+
export NANO_AGENTIC_WATCHDOG_MS=15000 # how often the watchdog checks channel liveness (default)
|
|
378
|
+
```
|
|
379
|
+
|
|
364
380
|
**Secure mode (opt-in).** For a deployment where you want the visibility channel
|
|
365
381
|
authenticated (rather than open on the LAN), start the server **and** every worker
|
|
366
382
|
box with the **same** `NANO_AGENTIC_SECRET` — same env-var name, same value on both
|
package/c8ctl-plugin.js
CHANGED
|
@@ -6089,6 +6089,138 @@ async function rediscoverAgenticUntilConnected({
|
|
|
6089
6089
|
return null;
|
|
6090
6090
|
}
|
|
6091
6091
|
|
|
6092
|
+
// Worker-side liveness watchdog defaults (jwulf/c8ctl-plugin-nano#144). A
|
|
6093
|
+
// previously-connected agentic channel that has been `disconnected` for longer
|
|
6094
|
+
// than the stale threshold — because the client lib's own reconnect never
|
|
6095
|
+
// brought it back (e.g. a half-open drop after a server restart/crash/partition,
|
|
6096
|
+
// or a reconnect that keeps failing) — is force-healed: the wedged channel is
|
|
6097
|
+
// torn down and full discovery + reopen is re-armed, instead of trusting the
|
|
6098
|
+
// client lib alone. Overridable via NANO_AGENTIC_STALE_MS / NANO_AGENTIC_WATCHDOG_MS.
|
|
6099
|
+
const DEFAULT_AGENTIC_STALE_MS = 60_000;
|
|
6100
|
+
const DEFAULT_AGENTIC_WATCHDOG_INTERVAL_MS = 15_000;
|
|
6101
|
+
|
|
6102
|
+
/**
|
|
6103
|
+
* Decide whether a worker's agentic channel is *stale* — i.e. it once connected,
|
|
6104
|
+
* is no longer connected, and has stayed down past `staleAfterMs`. Pure so the
|
|
6105
|
+
* watchdog's trigger condition is unit-testable without timers or sockets. A
|
|
6106
|
+
* channel that never opened (`everConnected() === false`) is NOT stale — it is
|
|
6107
|
+
* still doing its first connect, which the initial open / cold-start self-heal
|
|
6108
|
+
* owns. `disconnectedSince` is null whenever the channel is up (or never went
|
|
6109
|
+
* down), which also reads as not-stale.
|
|
6110
|
+
*
|
|
6111
|
+
* @param {{
|
|
6112
|
+
* connected: () => boolean,
|
|
6113
|
+
* everConnected: () => boolean,
|
|
6114
|
+
* disconnectedSince: () => (number|null),
|
|
6115
|
+
* now?: () => number,
|
|
6116
|
+
* staleAfterMs?: number,
|
|
6117
|
+
* }} opts
|
|
6118
|
+
* @returns {boolean}
|
|
6119
|
+
*/
|
|
6120
|
+
function agenticChannelIsStale({
|
|
6121
|
+
connected,
|
|
6122
|
+
everConnected,
|
|
6123
|
+
disconnectedSince,
|
|
6124
|
+
now = () => Date.now(),
|
|
6125
|
+
staleAfterMs = DEFAULT_AGENTIC_STALE_MS,
|
|
6126
|
+
}) {
|
|
6127
|
+
if (typeof connected !== 'function' || typeof everConnected !== 'function') return false;
|
|
6128
|
+
if (!everConnected()) return false; // never opened → the initial connect owns it
|
|
6129
|
+
if (connected()) return false; // healthy
|
|
6130
|
+
const since = typeof disconnectedSince === 'function' ? disconnectedSince() : null;
|
|
6131
|
+
if (since == null) return false; // no recorded drop → nothing to heal
|
|
6132
|
+
return now() - since >= staleAfterMs;
|
|
6133
|
+
}
|
|
6134
|
+
|
|
6135
|
+
/**
|
|
6136
|
+
* Start the worker-side agentic-channel liveness watchdog (#144). On a fixed
|
|
6137
|
+
* interval it asks {@link agenticChannelIsStale} whether the channel dropped and
|
|
6138
|
+
* never recovered within the threshold; when it has, it fires `onStale()` (which
|
|
6139
|
+
* tears the wedged channel down and re-runs discovery + reopen). It fires
|
|
6140
|
+
* `onStale` **exactly once per stale episode** — a per-episode latch is re-armed
|
|
6141
|
+
* only when the channel is next observed healthy (or gone), or when a heal
|
|
6142
|
+
* throws (a failed recovery retries on the next tick), so a persistent stale
|
|
6143
|
+
* condition with a successful heal does not retrigger the heal on every tick. Re-entrancy is
|
|
6144
|
+
* guarded so a slow heal never overlaps a later tick. Timers, clock, and the
|
|
6145
|
+
* channel accessors are injectable so this is unit-testable without real waits.
|
|
6146
|
+
* Returns a `{ stop, tick }` handle — `stop()` clears the timer AND latches the
|
|
6147
|
+
* watchdog stopped so any tick already scheduled or in flight around shutdown
|
|
6148
|
+
* becomes a no-op before it can reach `onStale` (shutdown relies on this to
|
|
6149
|
+
* prevent a stale-channel resurrection mid-teardown), and `tick()` runs a single
|
|
6150
|
+
* check (tests drive it directly).
|
|
6151
|
+
*
|
|
6152
|
+
* @param {{
|
|
6153
|
+
* getChannel: () => (import('./work-channel.mjs').WorkChannel | null),
|
|
6154
|
+
* disconnectedSince: () => (number|null),
|
|
6155
|
+
* onStale: () => (void|Promise<void>),
|
|
6156
|
+
* staleAfterMs?: number,
|
|
6157
|
+
* intervalMs?: number,
|
|
6158
|
+
* now?: () => number,
|
|
6159
|
+
* setIntervalFn?: typeof setInterval,
|
|
6160
|
+
* clearIntervalFn?: typeof clearInterval,
|
|
6161
|
+
* logger?: object|null,
|
|
6162
|
+
* }} opts
|
|
6163
|
+
* @returns {{ stop: () => void, tick: () => Promise<void> }}
|
|
6164
|
+
*/
|
|
6165
|
+
function startAgenticChannelWatchdog({
|
|
6166
|
+
getChannel,
|
|
6167
|
+
disconnectedSince,
|
|
6168
|
+
onStale,
|
|
6169
|
+
staleAfterMs = DEFAULT_AGENTIC_STALE_MS,
|
|
6170
|
+
intervalMs = DEFAULT_AGENTIC_WATCHDOG_INTERVAL_MS,
|
|
6171
|
+
now = () => Date.now(),
|
|
6172
|
+
setIntervalFn = setInterval,
|
|
6173
|
+
clearIntervalFn = clearInterval,
|
|
6174
|
+
logger = null,
|
|
6175
|
+
} = {}) {
|
|
6176
|
+
let healing = false;
|
|
6177
|
+
// Set once `stop()` runs so any tick already scheduled/in flight around
|
|
6178
|
+
// shutdown becomes a no-op and can never re-open the channel mid-teardown
|
|
6179
|
+
// (shutdown relies on `stop()` to prevent a stale-channel resurrection).
|
|
6180
|
+
let stopped = false;
|
|
6181
|
+
// Per-episode latch: fire `onStale` exactly once when a connected channel goes
|
|
6182
|
+
// stale, and don't fire again until it recovers (a fresh episode). Without this
|
|
6183
|
+
// the helper would re-heal on every tick whenever `onStale` does not itself
|
|
6184
|
+
// clear the staleness signal, causing repeated teardown/re-discovery attempts.
|
|
6185
|
+
let firedForEpisode = false;
|
|
6186
|
+
const tick = async () => {
|
|
6187
|
+
if (stopped || healing) return; // shutting down, or a heal is in flight — don't stack a second re-discovery
|
|
6188
|
+
const ch = typeof getChannel === 'function' ? getChannel() : null;
|
|
6189
|
+
// No channel object → the initial open or the cold-start self-heal loop owns
|
|
6190
|
+
// recovery; the watchdog only guards a channel that HAS connected and stalled.
|
|
6191
|
+
// A missing or non-stale (healthy / recovered / still-connecting) channel also
|
|
6192
|
+
// ends any current stale episode, so re-arm the latch for the next one.
|
|
6193
|
+
if (!ch || !agenticChannelIsStale({
|
|
6194
|
+
connected: () => ch.connected(),
|
|
6195
|
+
everConnected: () => ch.everConnected(),
|
|
6196
|
+
disconnectedSince,
|
|
6197
|
+
now,
|
|
6198
|
+
staleAfterMs,
|
|
6199
|
+
})) {
|
|
6200
|
+
firedForEpisode = false;
|
|
6201
|
+
return;
|
|
6202
|
+
}
|
|
6203
|
+
if (firedForEpisode) return; // already fired once for this stale episode
|
|
6204
|
+
healing = true;
|
|
6205
|
+
firedForEpisode = true;
|
|
6206
|
+
try {
|
|
6207
|
+
if (stopped) return; // shutdown raced us between the checks — do not heal
|
|
6208
|
+
const since = disconnectedSince();
|
|
6209
|
+
const downFor = since != null ? Math.round((now() - since) / 1000) : '?';
|
|
6210
|
+
logger?.warn?.(` agentic channel: no reconnect ${downFor}s after drop — forcing re-discovery (the client lib did not self-heal; likely a half-open drop).`);
|
|
6211
|
+
await onStale?.();
|
|
6212
|
+
} catch (err) {
|
|
6213
|
+
firedForEpisode = false; // heal failed → re-arm so a later tick retries this episode
|
|
6214
|
+
logger?.debug?.(`agentic watchdog heal failed: ${err?.message || err}`);
|
|
6215
|
+
} finally {
|
|
6216
|
+
healing = false;
|
|
6217
|
+
}
|
|
6218
|
+
};
|
|
6219
|
+
const timer = setIntervalFn(() => { tick().catch(() => {}); }, intervalMs);
|
|
6220
|
+
if (timer && typeof timer.unref === 'function') timer.unref();
|
|
6221
|
+
return { stop: () => { stopped = true; try { clearIntervalFn(timer); } catch { /* best effort */ } }, tick };
|
|
6222
|
+
}
|
|
6223
|
+
|
|
6092
6224
|
/**
|
|
6093
6225
|
* Collapse an agentic disconnect/failure detail into the single short string the
|
|
6094
6226
|
* marker's `agentic.message` field carries (#99 contract). Accepts the close
|
|
@@ -6587,6 +6719,17 @@ async function workAgent(req, flags) {
|
|
|
6587
6719
|
let workChannel = null;
|
|
6588
6720
|
/** @type {import('./work-buffer.mjs').BufferMonitor | null} */
|
|
6589
6721
|
let bufferMonitor = null;
|
|
6722
|
+
// #144 liveness watchdog state. `agenticDisconnectedSince` is the epoch-ms the
|
|
6723
|
+
// channel last dropped (null whenever it is up or has never opened); the
|
|
6724
|
+
// watchdog uses it to force a full re-discovery + reopen when the client lib's
|
|
6725
|
+
// own reconnect fails to bring a previously-connected channel back within the
|
|
6726
|
+
// stale threshold. `agenticWatchdog` is the running timer handle (stopped on
|
|
6727
|
+
// shutdown); `agenticSelfHealing` guards against two concurrent re-discovery
|
|
6728
|
+
// loops (the cold-start one and a watchdog-triggered one).
|
|
6729
|
+
let agenticDisconnectedSince = null;
|
|
6730
|
+
/** @type {{ stop: () => void } | null} */
|
|
6731
|
+
let agenticWatchdog = null;
|
|
6732
|
+
let agenticSelfHealing = false;
|
|
6590
6733
|
// Maintain `activeJobs` unconditionally: it feeds both the supervisor activity
|
|
6591
6734
|
// file (gated inside writeActivity) AND the agentic presence frame's live
|
|
6592
6735
|
// jobKey set, so a standalone worker (no NANO_SUPERVISOR_ACTIVITY_FILE) still
|
|
@@ -6696,11 +6839,21 @@ async function workAgent(req, flags) {
|
|
|
6696
6839
|
// (before these listeners existed), connected() is false but everConnected()
|
|
6697
6840
|
// is true — record that as `disconnected` rather than leaving it stuck at
|
|
6698
6841
|
// `connecting`.
|
|
6699
|
-
|
|
6700
|
-
|
|
6701
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
6842
|
+
// #144: track the drop clock alongside presence — a (re)connect clears it,
|
|
6843
|
+
// a disconnect starts it (first drop wins, so the watchdog measures from the
|
|
6844
|
+
// ORIGINAL drop, not the latest of a reconnect storm). The watchdog reads
|
|
6845
|
+
// this to decide when the client lib has failed to self-heal.
|
|
6846
|
+
workChannel.onConnect(() => { markAgentic('connected'); agenticDisconnectedSince = null; });
|
|
6847
|
+
workChannel.onReconnect(() => { markAgentic('connected'); agenticDisconnectedSince = null; });
|
|
6848
|
+
workChannel.onDisconnect((info) => {
|
|
6849
|
+
markAgentic('disconnected', normalizeAgenticMessage(info));
|
|
6850
|
+
if (agenticDisconnectedSince == null) agenticDisconnectedSince = Date.now();
|
|
6851
|
+
});
|
|
6852
|
+
if (workChannel.connected()) { markAgentic('connected'); agenticDisconnectedSince = null; }
|
|
6853
|
+
else if (workChannel.everConnected()) {
|
|
6854
|
+
markAgentic('disconnected');
|
|
6855
|
+
if (agenticDisconnectedSince == null) agenticDisconnectedSince = Date.now();
|
|
6856
|
+
}
|
|
6704
6857
|
} catch (err) {
|
|
6705
6858
|
// Never let a channel failure stop the worker from doing its actual job.
|
|
6706
6859
|
workChannel = null;
|
|
@@ -6732,16 +6885,16 @@ async function workAgent(req, flags) {
|
|
|
6732
6885
|
}
|
|
6733
6886
|
};
|
|
6734
6887
|
|
|
6735
|
-
|
|
6736
|
-
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
6744
|
-
|
|
6888
|
+
// (A) Background self-heal loop, shared by the cold-start advisory path (#133)
|
|
6889
|
+
// AND the #144 liveness watchdog. Re-run discovery on a jittered backoff and,
|
|
6890
|
+
// on the first `connect` target, (re)open the channel WITHOUT a restart. The
|
|
6891
|
+
// `agenticSelfHealing` guard makes it idempotent: the watchdog can call it
|
|
6892
|
+
// after tearing a stale channel down without racing a still-running cold-start
|
|
6893
|
+
// loop. A shared cache lets a brief blip reuse the last known-good hub (#133-C).
|
|
6894
|
+
const armAgenticSelfHeal = () => {
|
|
6895
|
+
if (agenticSelfHealing) return; // a re-discovery loop is already running
|
|
6896
|
+
if (workChannel !== null) return; // a channel already exists — nothing to heal
|
|
6897
|
+
agenticSelfHealing = true;
|
|
6745
6898
|
const hubCache = new Map();
|
|
6746
6899
|
rediscoverAgenticUntilConnected({
|
|
6747
6900
|
resolveTarget: () => resolveAgenticTarget({ camunda, logger, cache: hubCache }),
|
|
@@ -6749,7 +6902,7 @@ async function workAgent(req, flags) {
|
|
|
6749
6902
|
agenticCfg = target.config;
|
|
6750
6903
|
agenticState = agenticStateForTarget(target, safeAgenticDisplayUrl);
|
|
6751
6904
|
writeActivity();
|
|
6752
|
-
logger.info(' agentic channel: background re-discovery succeeded —
|
|
6905
|
+
logger.info(' agentic channel: background re-discovery succeeded — (re)opening channel.');
|
|
6753
6906
|
await openAgenticChannel(agenticCfg);
|
|
6754
6907
|
// openAgenticChannel swallows its own open failures (it nulls
|
|
6755
6908
|
// workChannel and returns rather than throwing), so a failed open must
|
|
@@ -6762,7 +6915,57 @@ async function workAgent(req, flags) {
|
|
|
6762
6915
|
// Stop as soon as a channel exists (loop won this or a prior attempt did).
|
|
6763
6916
|
shouldContinue: () => workChannel === null,
|
|
6764
6917
|
logger,
|
|
6765
|
-
})
|
|
6918
|
+
})
|
|
6919
|
+
.catch(() => { /* best-effort self-heal — never surfaces an error */ })
|
|
6920
|
+
.finally(() => { agenticSelfHealing = false; });
|
|
6921
|
+
};
|
|
6922
|
+
|
|
6923
|
+
// (B) #144 liveness watchdog: force-heal a wedged channel. When a channel that
|
|
6924
|
+
// HAS connected drops and the client lib's own reconnect never brings it back
|
|
6925
|
+
// within the stale threshold (a half-open drop after a server restart/crash/
|
|
6926
|
+
// partition, or a reconnect that keeps failing), the client sits `disconnected`
|
|
6927
|
+
// forever and the worker vanishes from the Workers view until a supervisor
|
|
6928
|
+
// restart. This tears the wedged channel down (so `shouldContinue` re-arms) and
|
|
6929
|
+
// re-runs full discovery + reopen instead of trusting the client lib alone.
|
|
6930
|
+
const healStaleAgenticChannel = async () => {
|
|
6931
|
+
const stale = workChannel;
|
|
6932
|
+
if (!stale) return;
|
|
6933
|
+
workChannel = null; // re-arms armAgenticSelfHeal()'s shouldContinue gate
|
|
6934
|
+
agenticDisconnectedSince = null; // reset the clock; the fresh open restarts it
|
|
6935
|
+
try { bufferMonitor?.stop(); } catch { /* best effort */ }
|
|
6936
|
+
bufferMonitor = null;
|
|
6937
|
+
markAgentic('disconnected', 'stale channel — re-discovering hub');
|
|
6938
|
+
// Deregister + close the wedged client so it stops its own doomed reconnect
|
|
6939
|
+
// attempts and we don't leak two clients once the fresh one connects.
|
|
6940
|
+
try { await stale.stop('stale channel — re-discovering'); } catch { /* best effort */ }
|
|
6941
|
+
armAgenticSelfHeal();
|
|
6942
|
+
};
|
|
6943
|
+
|
|
6944
|
+
const startAgenticWatchdog = () => {
|
|
6945
|
+
if (agenticWatchdog) return;
|
|
6946
|
+
const staleAfterMs = Math.max(5_000, intFlag(process.env.NANO_AGENTIC_STALE_MS, DEFAULT_AGENTIC_STALE_MS));
|
|
6947
|
+
const intervalMs = Math.max(1_000, intFlag(process.env.NANO_AGENTIC_WATCHDOG_MS, DEFAULT_AGENTIC_WATCHDOG_INTERVAL_MS));
|
|
6948
|
+
agenticWatchdog = startAgenticChannelWatchdog({
|
|
6949
|
+
getChannel: () => workChannel,
|
|
6950
|
+
disconnectedSince: () => agenticDisconnectedSince,
|
|
6951
|
+
onStale: healStaleAgenticChannel,
|
|
6952
|
+
staleAfterMs,
|
|
6953
|
+
intervalMs,
|
|
6954
|
+
logger,
|
|
6955
|
+
});
|
|
6956
|
+
};
|
|
6957
|
+
|
|
6958
|
+
if (agenticCfg) {
|
|
6959
|
+
await openAgenticChannel(agenticCfg);
|
|
6960
|
+
// Guard the connected channel: if it later drops and the client lib can't
|
|
6961
|
+
// recover it, the watchdog forces a full re-discovery + reopen (#144).
|
|
6962
|
+
startAgenticWatchdog();
|
|
6963
|
+
} else if (agenticTarget.status === 'advisory') {
|
|
6964
|
+
// A cold-start discovery miss leaves the worker `advisory`; the self-heal
|
|
6965
|
+
// loop upgrades it to `connected` without a restart (#133), and once a
|
|
6966
|
+
// channel exists the watchdog keeps it alive across later drops (#144).
|
|
6967
|
+
armAgenticSelfHeal();
|
|
6968
|
+
startAgenticWatchdog();
|
|
6766
6969
|
}
|
|
6767
6970
|
|
|
6768
6971
|
// C3 (#42): the role's live-terminal mode — a full PTY (streamed on the relay
|
|
@@ -7382,6 +7585,9 @@ async function workAgent(req, flags) {
|
|
|
7382
7585
|
logger.info(`Received ${signal} — stopping ${list.length} worker(s)...`);
|
|
7383
7586
|
if (reaperTimer) clearInterval(reaperTimer);
|
|
7384
7587
|
if (runDirTimer) clearInterval(runDirTimer);
|
|
7588
|
+
// Stop the #144 liveness watchdog so it can't kick off a re-discovery
|
|
7589
|
+
// mid-teardown (which would resurrect the channel we're about to close).
|
|
7590
|
+
if (agenticWatchdog) { try { agenticWatchdog.stop(); } catch { /* best effort */ } agenticWatchdog = null; }
|
|
7385
7591
|
const results = await Promise.all(list.map(drainWorker));
|
|
7386
7592
|
const stopFailures = results.filter((ok) => !ok).length;
|
|
7387
7593
|
if (stopFailures > 0) {
|
|
@@ -11751,6 +11957,8 @@ export {
|
|
|
11751
11957
|
isLinkLocalAddress,
|
|
11752
11958
|
rediscoverAgenticUntilConnected,
|
|
11753
11959
|
defaultAgenticRediscoveryDelays,
|
|
11960
|
+
agenticChannelIsStale,
|
|
11961
|
+
startAgenticChannelWatchdog,
|
|
11754
11962
|
};
|
|
11755
11963
|
export { compareSemver, githubRepoSlug, filterReleasesSince, renderReleaseBody };
|
|
11756
11964
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c8ctl-plugin-nano",
|
|
3
|
-
"version": "1.44.
|
|
3
|
+
"version": "1.44.9",
|
|
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.9",
|
|
61
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.44.9",
|
|
62
|
+
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.44.9",
|
|
63
|
+
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.44.9",
|
|
64
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.44.9",
|
|
65
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.44.9",
|
|
66
|
+
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.44.9"
|
|
67
67
|
}
|
|
68
68
|
}
|
package/work-channel.mjs
CHANGED
|
@@ -281,15 +281,25 @@ export async function createWorkChannel(opts) {
|
|
|
281
281
|
everConnected: () => hasConnected,
|
|
282
282
|
buffered: () => client.buffered,
|
|
283
283
|
async stop(reason = 'worker stopped') {
|
|
284
|
+
// Deregister to drop presence cleanly, then ALWAYS close the socket so the
|
|
285
|
+
// client stops its own reconnect loop. Closing only on a deregister error
|
|
286
|
+
// (the old behaviour) left a successfully-deregistered client half-open and
|
|
287
|
+
// still reconnecting — which is exactly the wedged/duplicate-client case the
|
|
288
|
+
// #144 stale-channel heal path relies on stop() to end.
|
|
289
|
+
// deregister() is fire-and-forget but may be thenable (register() is
|
|
290
|
+
// treated as one above), so guard BOTH a synchronous throw and an async
|
|
291
|
+
// rejection — an unhandled rejection during shutdown must never escape.
|
|
284
292
|
try {
|
|
285
|
-
client.deregister(reason)
|
|
286
|
-
} catch (err) {
|
|
287
|
-
try {
|
|
293
|
+
Promise.resolve(client.deregister(reason)).catch((err) => {
|
|
288
294
|
log.warn?.(`agentic deregister failed: ${err?.message || err}`);
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
295
|
+
});
|
|
296
|
+
} catch (err) {
|
|
297
|
+
log.warn?.(`agentic deregister failed: ${err?.message || err}`);
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
client.close();
|
|
301
|
+
} catch {
|
|
302
|
+
/* best effort — never let shutdown hang on the channel */
|
|
293
303
|
}
|
|
294
304
|
},
|
|
295
305
|
};
|