c8ctl-plugin-nano 1.44.4 → 1.44.6
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 +13 -0
- package/c8ctl-plugin.js +60 -0
- package/package.json +8 -8
- package/work-relay.mjs +28 -0
package/README.md
CHANGED
|
@@ -262,6 +262,19 @@ specialisation.
|
|
|
262
262
|
> (capability-declared) enrolment path. Use `--auto-scope <process-id | prefix>`
|
|
263
263
|
> to narrow the blast radius to one app/network.
|
|
264
264
|
|
|
265
|
+
> **Dual-stack / IPv6-first engine hosts.** The engine client races IPv4 and IPv6
|
|
266
|
+
> (Happy-Eyeballs, RFC 8305) at connect time, so a worker whose engine host
|
|
267
|
+
> resolves to an **unreachable IPv6 address first** — common with macOS mDNS
|
|
268
|
+
> (`merlin.local → fe80::… (dead) → 192.168.x.x`), a stray/misordered `AAAA`, or
|
|
269
|
+
> an IPv6-advertised host — transparently falls back to IPv4 instead of failing
|
|
270
|
+
> the activation / `--auto` engine read (`fetch failed` / `write EPIPE` /
|
|
271
|
+
> `UND_ERR_CONNECT_TIMEOUT`). Without this, the `--auto` autoscaler would read
|
|
272
|
+
> `0` job types and **silently scale the fleet to zero** (workers vanish from the
|
|
273
|
+
> Nano console) even though `curl`/`fetch` reach the same host fine
|
|
274
|
+
> (jwulf/c8ctl-plugin-nano#139). No configuration is needed; if you still want to
|
|
275
|
+
> pin a family you can set `NODE_OPTIONS=--dns-result-order=ipv4first` or point
|
|
276
|
+
> the engine URL at the IPv4 literal.
|
|
277
|
+
|
|
265
278
|
|
|
266
279
|
The optional `--name` sets **this worker's name** — the `workerName` it
|
|
267
280
|
registers under at the broker (`‹name›:‹jobType›`) and how it shows up in
|
package/c8ctl-plugin.js
CHANGED
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
unwatchFile,
|
|
52
52
|
} from 'node:fs';
|
|
53
53
|
import { createConnection, createServer } from 'node:net';
|
|
54
|
+
import * as nodeNet from 'node:net';
|
|
54
55
|
import { lookup as dnsLookup } from 'node:dns/promises';
|
|
55
56
|
import { randomUUID, createHash, randomBytes } from 'node:crypto';
|
|
56
57
|
import { homedir, platform as osPlatform, devNull, tmpdir, hostname } from 'node:os';
|
|
@@ -6045,6 +6046,58 @@ function buildActivityPayload({ pid, updatedAt, jobs, engine, agentic }) {
|
|
|
6045
6046
|
};
|
|
6046
6047
|
}
|
|
6047
6048
|
|
|
6049
|
+
/**
|
|
6050
|
+
* Enable Happy-Eyeballs (RFC 8305) dual-stack connect for the engine client so a
|
|
6051
|
+
* worker whose engine host resolves IPv6-first with an *unreachable* v6 address
|
|
6052
|
+
* (macOS mDNS `fe80::` link-local, a stray/misordered AAAA, an IPv6-advertised
|
|
6053
|
+
* host) transparently races and falls back to IPv4 — matching `curl`/`fetch` —
|
|
6054
|
+
* instead of connecting to the dead address and failing the activation / `--auto`
|
|
6055
|
+
* engine read with `fetch failed` / `write EPIPE` / `UND_ERR_CONNECT_TIMEOUT`
|
|
6056
|
+
* (jwulf/c8ctl-plugin-nano#139).
|
|
6057
|
+
*
|
|
6058
|
+
* We set Node's PROCESS-WIDE `net` default rather than an instance option so the
|
|
6059
|
+
* fix covers the *class*, not one call site: BOTH transports the worker harness
|
|
6060
|
+
* uses inherit it — the `@camunda8` SDK client's `activateJobs` (its undici
|
|
6061
|
+
* dispatcher connects via `net`) AND the raw-`fetch` `--auto` reconcile / initial
|
|
6062
|
+
* engine reads. Node ≥20 already defaults this on, but older runtimes — and any
|
|
6063
|
+
* build where the client's dispatcher inherits a `false` default — leave it off;
|
|
6064
|
+
* the reporter's repro shows the failing "SDK default path" is exactly
|
|
6065
|
+
* `setDefaultAutoSelectFamily(false)`, and asserting `true` here is what fixes it.
|
|
6066
|
+
* The attempt timeout bounds the per-address race so a dead `fe80::` yields to
|
|
6067
|
+
* IPv4 quickly (RFC 8305 default of 250ms) instead of stalling the connect.
|
|
6068
|
+
*
|
|
6069
|
+
* Fail-open and idempotent: a runtime without the API (or any throw) is swallowed
|
|
6070
|
+
* so Happy-Eyeballs — an optimisation — can never block a worker from starting.
|
|
6071
|
+
*
|
|
6072
|
+
* @param {{ net?: object, attemptTimeoutMs?: number }} [opts] injection seam for tests
|
|
6073
|
+
* @returns {boolean} true if the net default was (re)asserted to Happy-Eyeballs
|
|
6074
|
+
*/
|
|
6075
|
+
function enableEngineHappyEyeballs(opts = {}) {
|
|
6076
|
+
try {
|
|
6077
|
+
// Destructure INSIDE the try so a non-object arg (e.g. `null`) fails open
|
|
6078
|
+
// like any other throw rather than blowing up before the guard.
|
|
6079
|
+
const { net = nodeNet, attemptTimeoutMs = 250 } = opts || {};
|
|
6080
|
+
if (net && typeof net.setDefaultAutoSelectFamily === 'function') {
|
|
6081
|
+
net.setDefaultAutoSelectFamily(true);
|
|
6082
|
+
// The attempt timeout is an optional refinement: once the primary family
|
|
6083
|
+
// default is asserted the contract is satisfied, so a throw here must not
|
|
6084
|
+
// retract our `true`. Swallow it independently.
|
|
6085
|
+
try {
|
|
6086
|
+
if (typeof net.setDefaultAutoSelectFamilyAttemptTimeout === 'function'
|
|
6087
|
+
&& Number.isFinite(attemptTimeoutMs) && attemptTimeoutMs > 0) {
|
|
6088
|
+
net.setDefaultAutoSelectFamilyAttemptTimeout(attemptTimeoutMs);
|
|
6089
|
+
}
|
|
6090
|
+
} catch {
|
|
6091
|
+
// Fail-open on the optional timeout setter; the family default still holds.
|
|
6092
|
+
}
|
|
6093
|
+
return true;
|
|
6094
|
+
}
|
|
6095
|
+
} catch {
|
|
6096
|
+
// Fail-open: Happy-Eyeballs is a connectivity optimisation, never a start gate.
|
|
6097
|
+
}
|
|
6098
|
+
return false;
|
|
6099
|
+
}
|
|
6100
|
+
|
|
6048
6101
|
/**
|
|
6049
6102
|
* work — turn a hire profile into live Nano job workers (one per job-type in
|
|
6050
6103
|
* the rank×capability matrix) and poll for work in the foreground until Ctrl-C.
|
|
@@ -6279,6 +6332,12 @@ async function workAgent(req, flags) {
|
|
|
6279
6332
|
logger.error('--auto-scope requires --auto (it narrows the engine-read agent job types).');
|
|
6280
6333
|
process.exit(1);
|
|
6281
6334
|
}
|
|
6335
|
+
// Enable Happy-Eyeballs (RFC 8305) at the socket layer BEFORE the SDK client
|
|
6336
|
+
// or any engine read is created, so a worker whose engine host resolves
|
|
6337
|
+
// IPv6-first with a dead v6 address falls back to IPv4 instead of silently
|
|
6338
|
+
// scaling the fleet to zero (jwulf/c8ctl-plugin-nano#139). Process-wide, so it
|
|
6339
|
+
// covers both the SDK's activateJobs and the raw-fetch --auto engine reads.
|
|
6340
|
+
enableEngineHappyEyeballs();
|
|
6282
6341
|
const camunda = globalThis.c8ctl.createClient();
|
|
6283
6342
|
|
|
6284
6343
|
// Broker REST endpoint for live linked-resource prompts (issue #63) and the
|
|
@@ -11612,6 +11671,7 @@ export {
|
|
|
11612
11671
|
serviceTaskHasAgentHeader,
|
|
11613
11672
|
readDeployedAgentJobTypes,
|
|
11614
11673
|
resolveAutoJobTypes,
|
|
11674
|
+
enableEngineHappyEyeballs,
|
|
11615
11675
|
workAgent,
|
|
11616
11676
|
derivePollTimeoutMs,
|
|
11617
11677
|
AGENT_TASK_NS,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "c8ctl-plugin-nano",
|
|
3
|
-
"version": "1.44.
|
|
3
|
+
"version": "1.44.6",
|
|
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.6",
|
|
61
|
+
"@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.44.6",
|
|
62
|
+
"@nanobpm/c8ctl-plugin-nano-linux-x64": "1.44.6",
|
|
63
|
+
"@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.44.6",
|
|
64
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.44.6",
|
|
65
|
+
"@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.44.6",
|
|
66
|
+
"@nanobpm/c8ctl-plugin-nano-win32-x64": "1.44.6"
|
|
67
67
|
}
|
|
68
68
|
}
|
package/work-relay.mjs
CHANGED
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
// a duck-typed terminal handle, so it is unit-testable with a fake terminal and
|
|
24
24
|
// a fake channel.
|
|
25
25
|
|
|
26
|
+
import { transcript as agenticTranscript } from './agentic.mjs';
|
|
27
|
+
|
|
26
28
|
/** Prefix for a per-job relay stream name. One stream carries a job's terminal. */
|
|
27
29
|
export const RELAY_STREAM_PREFIX = 'job:';
|
|
28
30
|
|
|
@@ -40,6 +42,26 @@ export function relayStreamName(jobKey) {
|
|
|
40
42
|
return `${RELAY_STREAM_PREFIX}${String(jobKey)}`;
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
/**
|
|
46
|
+
* The initial produce frame emitted on a relay stream the instant a session is
|
|
47
|
+
* created: a canonical `@nanobpm/agentic` lifecycle "open" transcript event.
|
|
48
|
+
*
|
|
49
|
+
* It carries no agent output — its sole job is to OPEN the `job:<jobKey>` stream
|
|
50
|
+
* immediately so the app's correlation registry links this worker→jobKey on
|
|
51
|
+
* session creation, INDEPENDENT of whether the agent later emits any transcript
|
|
52
|
+
* bytes. Without it, a quiet ACP agent (one that emits no mappable
|
|
53
|
+
* `session/update`) never produces a `job:` frame, so the app never correlates
|
|
54
|
+
* the job — it renders "—" in the cockpit with no drill-in, even though the job
|
|
55
|
+
* ran and completed (regression since #128 coupled correlation to transcript
|
|
56
|
+
* bytes). Newline-framed to match the transcript-chunk framing the ACP relay
|
|
57
|
+
* path uses, and derived through `encodeTranscriptEvent` (never hand-rolled) so
|
|
58
|
+
* the wire marker stays single-sourced in `@nanobpm/agentic`.
|
|
59
|
+
*/
|
|
60
|
+
export const RELAY_OPEN_CHUNK = `${agenticTranscript.encodeTranscriptEvent({
|
|
61
|
+
kind: 'lifecycle',
|
|
62
|
+
phase: 'open',
|
|
63
|
+
})}\n`;
|
|
64
|
+
|
|
43
65
|
/**
|
|
44
66
|
* Resolve a role's terminal mode — whether the agent harness for this role gets
|
|
45
67
|
* a full PTY or a plain pipe. Honors the vocab's per-role opt-in: a role may set
|
|
@@ -189,5 +211,11 @@ export function createRelaySession({ channel, jobKey, logger } = {}) {
|
|
|
189
211
|
for (const detach of [...activeDetaches]) detach();
|
|
190
212
|
};
|
|
191
213
|
|
|
214
|
+
// Open the stream the instant the session exists, so the app correlates
|
|
215
|
+
// worker→jobKey immediately — independent of any later agent transcript bytes
|
|
216
|
+
// (a quiet ACP agent emits none). Routed through the internal relay(), which
|
|
217
|
+
// already swallows sink errors, so opening the stream never fails the job.
|
|
218
|
+
relay(RELAY_OPEN_CHUNK);
|
|
219
|
+
|
|
192
220
|
return { stream, relay, attachSteer, close };
|
|
193
221
|
}
|