@nanobpm/nano-workforce 0.114.1 → 0.115.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/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # [0.115.0](https://github.com/nanobpm/nano-workforce/compare/v0.114.1...v0.115.0) (2026-08-20)
2
+
3
+
4
+ ### Features
5
+
6
+ * **engine:** startup preflight + canonical engine address resolution ([#391](https://github.com/nanobpm/nano-workforce/issues/391)) ([#404](https://github.com/nanobpm/nano-workforce/issues/404)) ([16fcb71](https://github.com/nanobpm/nano-workforce/commit/16fcb711f190341f03fd84914fa66c1ee704b78c)), closes [Magikcraft/nano-bpm#940](https://github.com/Magikcraft/nano-bpm/issues/940)
7
+
1
8
  ## [0.114.1](https://github.com/nanobpm/nano-workforce/compare/v0.114.0...v0.114.1) (2026-08-20)
2
9
 
3
10
 
package/README.md CHANGED
@@ -252,7 +252,7 @@ active epic already targets the same custom base. See
252
252
  |---|---|---|
253
253
  | `PR_REVIEW_PORT` | `3000` | app HTTP port |
254
254
  | `NANO_APP_DB_URL` | `file:./app.db` | sqlite datasource |
255
- | `NANOBPMN_BASE_URL` | `http://localhost:8080` | engine base URL (or set `CAMUNDA_REST_ADDRESS` to the `/v2` REST address directly) |
255
+ | `NANOBPMN_BASE_URL` | `http://localhost:8080` | engine base URL (or set `CAMUNDA_REST_ADDRESS` to the `/v2` REST address directly). See [Engine address & the startup preflight](#engine-address--the-startup-preflight) |
256
256
  | `GITHUB_TOKEN` | — | token for the review poller / merge (or use the host `gh` CLI) |
257
257
  | `NANO_PR_GITHUB_TRANSPORT` | `auto` | how the poller reads GitHub: `gh` (host CLI), `token` (`GITHUB_TOKEN` over HTTP), or `auto` |
258
258
  | `NANO_PR_POLL_MS` | `60000` | review-ready poll interval |
@@ -267,6 +267,49 @@ active epic already targets the same custom base. See
267
267
  | `NANO_WORKFORCE_BASE_URL` | `http://localhost:3000` | externally-reachable base URL for the capability hooks (`/app/api/hooks/*`). Must resolve from **wherever the agent runs** — set it to the app's LAN address (or console-proxy URL) for a remote fleet. See [Fleet networking](#fleet-networking-remote-workers) |
268
268
  | `NANO_AGENTIC_SECRET` | — | enables **secure mode** for the agentic visibility channel (`/agentic`): every peer must present the **same** `NANO_AGENTIC_SECRET` value (set the identical env var on the server and every worker box — Tab A → Slot A). Unset = on-by-default **LOCAL mode** — the well-known token is honoured from **any origin** (open on the trusted LAN, matching the engine's posture); exposure is governed by the server bind address, not a shared secret. Also accepts `NANO_PR_WEBHOOK_SECRET` |
269
269
 
270
+ ### Engine address & the startup preflight
271
+
272
+ The app talks to one engine over the Camunda 8 REST API. The REST address is
273
+ resolved with a fixed precedence — set **one** of:
274
+
275
+ 1. **`CAMUNDA_REST_ADDRESS`** — used verbatim (it already points at the `/v2`
276
+ REST address, e.g. `http://engine.example:8080/v2`). **Wins** if set.
277
+ 2. **`NANOBPMN_BASE_URL`** — the engine *base*; the app appends `/v2`
278
+ (e.g. `http://localhost:7000` → `http://localhost:7000/v2`).
279
+ 3. Neither set → the base defaults to **`http://localhost:8080`**.
280
+
281
+ At boot the app **echoes the resolved address and which input it came from**,
282
+ then probes `/v2/topology` and announces **which engine answered** — so a
283
+ misconfigured address is obvious immediately instead of surfacing later as a
284
+ cryptic mid-run engine error:
285
+
286
+ ```
287
+ Engine address: http://localhost:8080/v2 (from default (http://localhost:8080))
288
+ Engine: Nano engine (nanobpmn v0.114.1) — Falcon streaming at /falcon at http://localhost:8080/v2.
289
+ ```
290
+
291
+ The preflight is **informational, never a gate** — Nano Workforce runs against a
292
+ stock **Camunda 8** cluster too, so a non-Nano engine is announced
293
+ (`Engine: Camunda 8 (gateway v8.x) — REST only …`), not rejected. If nothing
294
+ answers, it logs a `warn` (`could not reach … features will fail to start until
295
+ the engine is reachable`) and boot continues. On a **secured** cluster the probe
296
+ sends `CAMUNDA_TOKEN` as a bearer credential; a `401/403` is reported as an auth
297
+ hint (check `CAMUNDA_TOKEN`), not as an unreachable engine.
298
+
299
+ > **Watch the port when launched from a console.** A console-launched app can
300
+ > default `NANOBPMN_BASE_URL` to `http://localhost:8080`. If **another Camunda 8**
301
+ > is already on `:8080`, the app will talk to *that* engine (it works — C8 is
302
+ > supported), which may not be the engine you intended. Check the startup
303
+ > `Engine:` line; to target a Nano engine on a different port, set
304
+ > `NANOBPMN_BASE_URL` (or `CAMUNDA_REST_ADDRESS`) explicitly.
305
+ >
306
+ > When the app *can't* work against the reached engine you'll see a job/instance
307
+ > decode error at first feature start — `MalformedFrameError` / `MalformedJobError`
308
+ > (Falcon) or the Camunda REST client's own `4xx` (older versions surfaced
309
+ > `engine response missing processInstanceKey/key`). The startup `Engine:` line
310
+ > tells you which engine you actually reached, before any such error.
311
+
312
+
270
313
  ### Fleet networking (remote workers)
271
314
 
272
315
  `nano-workforce` can drive a **distributed worker fleet** — `senior:*` agents running on other LAN
@@ -20,6 +20,7 @@ import {
20
20
  import type { RegisteredWorker } from "@nanobpm/agentic/vocab";
21
21
  import type { Logger } from "@nanobpm/urban";
22
22
  import type { RegistryReport as WireRegistryReport } from "../../../nano-generated/api-io.d.ts";
23
+ import { resolveEngineAddress } from "../../enginePreflight.ts";
23
24
  import { envVar } from "../../version.ts";
24
25
  import { currentPresenceRegistry } from "../families/presence.family.ts";
25
26
  import { CREW_VOCAB_VERSION, crewResolver } from "./crew-vocab.ts";
@@ -40,17 +41,14 @@ export interface RegistryReport extends DemandSupplyReport {
40
41
  }
41
42
 
42
43
  /**
43
- * Derive the engine's C8 v2 REST base the demand reader targets the same precedence/derivation
44
- * `main.ts` uses: an explicit `CAMUNDA_REST_ADDRESS` wins, else it is derived from `NANOBPMN_BASE_URL`
45
- * (+ `/v2`), defaulting to `http://localhost:8080/v2`. Read through the declared env schema (ADR 0004).
46
- * (This reader additionally strips trailing slashes off an explicit `CAMUNDA_REST_ADDRESS`, which
47
- * `main.ts` does not, so the derived base is well-formed regardless of a trailing `/`.)
44
+ * Derive the engine's C8 v2 REST base the demand reader targets. Delegates to
45
+ * the canonical {@link resolveEngineAddress} (the single source of truth shared
46
+ * with `main.ts`) so the precedence explicit `CAMUNDA_REST_ADDRESS` wins, else
47
+ * `NANOBPMN_BASE_URL` (+ `/v2`), defaulting to `http://localhost:8080/v2` lives
48
+ * in one place and cannot drift between call sites.
48
49
  */
49
50
  export function engineRestAddress(): string {
50
- const explicit = envVar("CAMUNDA_REST_ADDRESS")?.replace(/\/+$/, "");
51
- if (explicit) return explicit;
52
- const base = (envVar("NANOBPMN_BASE_URL") ?? "http://localhost:8080").replace(/\/+$/, "");
53
- return `${base}/v2`;
51
+ return resolveEngineAddress().restAddress;
54
52
  }
55
53
 
56
54
  /** The live supply rows (`{ instance, capability }`) from the H1 presence registry, or none when unmounted. */
@@ -0,0 +1,122 @@
1
+ // Tests for engine address resolution + the startup preflight (nano-workforce#391).
2
+ //
3
+ // The resolver and the identity description are pure (an injectable env reader /
4
+ // a plain topology body), and the preflight takes an injectable `fetch`, so none
5
+ // of this needs a live engine or `process.env` mutation.
6
+ import { test } from "node:test";
7
+ import { assert, assertEquals } from "#test-assert";
8
+ import {
9
+ announceEngine,
10
+ describeEngine,
11
+ type EngineAddress,
12
+ resolveEngineAddress,
13
+ type TopologyProbe,
14
+ } from "./enginePreflight.ts";
15
+
16
+ const reader = (vars: Record<string, string>) => (name: string): string | null => vars[name] ?? null;
17
+
18
+ test("resolveEngineAddress honours an explicit CAMUNDA_REST_ADDRESS and strips trailing slashes", () => {
19
+ const addr = resolveEngineAddress(reader({ CAMUNDA_REST_ADDRESS: "http://engine.example:8080/v2///" }));
20
+ assertEquals(addr, { restAddress: "http://engine.example:8080/v2", source: "CAMUNDA_REST_ADDRESS" });
21
+ });
22
+
23
+ test("resolveEngineAddress derives /v2 from NANOBPMN_BASE_URL when no explicit address", () => {
24
+ const addr = resolveEngineAddress(reader({ NANOBPMN_BASE_URL: "http://engine.example:7000//" }));
25
+ assertEquals(addr, { restAddress: "http://engine.example:7000/v2", source: "NANOBPMN_BASE_URL" });
26
+ });
27
+
28
+ test("resolveEngineAddress defaults to localhost:8080 and labels the source as the default", () => {
29
+ const addr = resolveEngineAddress(reader({}));
30
+ assertEquals(addr, { restAddress: "http://localhost:8080/v2", source: "default (http://localhost:8080)" });
31
+ });
32
+
33
+ test("describeEngine reports a Nano engine from the `nano` marker", () => {
34
+ const line = describeEngine({ nano: { engine: "nanobpmn", version: "0.114.1", falconPath: "/falcon" } });
35
+ assert(line.includes("Nano engine (nanobpmn v0.114.1)"), "names the nano engine + version");
36
+ assert(line.includes("/falcon"), "mentions the Falcon path");
37
+ });
38
+
39
+ test("describeEngine reports Camunda 8 (never rejects) when the `nano` marker is absent", () => {
40
+ const line = describeEngine({ gatewayVersion: "8.6.0" });
41
+ assert(line.startsWith("Camunda 8"), "identifies Camunda 8");
42
+ assert(line.includes("8.6.0"), "surfaces the gateway version");
43
+ assert(line.includes("REST only"), "notes Falcon is unavailable");
44
+ });
45
+
46
+ test("describeEngine tolerates an empty / missing body", () => {
47
+ assert(describeEngine(null).startsWith("Camunda 8"), "null body degrades to Camunda 8");
48
+ assert(describeEngine({}).startsWith("Camunda 8"), "empty body degrades to Camunda 8");
49
+ });
50
+
51
+ // --- announceEngine (injected fetch; never throws) ---
52
+
53
+ const ADDR: EngineAddress = { restAddress: "http://localhost:8080/v2", source: "default (http://localhost:8080)" };
54
+
55
+ function capture() {
56
+ const info: string[] = [];
57
+ const warn: string[] = [];
58
+ return { log: { info: (m: string) => info.push(m), warn: (m: string) => warn.push(m) }, info, warn };
59
+ }
60
+
61
+ const okResponse = (body: TopologyProbe) =>
62
+ ({ ok: true, status: 200, json: () => Promise.resolve(body) }) as unknown as Response;
63
+
64
+ test("announceEngine logs the resolved address + a Nano identity line", async () => {
65
+ const { log, info, warn } = capture();
66
+ let probed = "";
67
+ await announceEngine(ADDR, log, {
68
+ fetchImpl: ((url: string) => {
69
+ probed = url;
70
+ return Promise.resolve(okResponse({ nano: { engine: "nanobpmn", version: "1.2.3", falconPath: "/falcon" } }));
71
+ }) as unknown as typeof fetch,
72
+ });
73
+ assertEquals(probed, "http://localhost:8080/v2/topology");
74
+ assert(info.some((l) => l.includes("Engine address: http://localhost:8080/v2 (from default")), "echoes address+source");
75
+ assert(info.some((l) => l.includes("Nano engine (nanobpmn v1.2.3)")), "announces the Nano engine");
76
+ assertEquals(warn, []);
77
+ });
78
+
79
+ test("announceEngine announces Camunda 8 without rejecting", async () => {
80
+ const { log, info, warn } = capture();
81
+ await announceEngine(ADDR, log, {
82
+ fetchImpl: (() => Promise.resolve(okResponse({ gatewayVersion: "8.6.0" }))) as unknown as typeof fetch,
83
+ });
84
+ assert(info.some((l) => l.includes("Camunda 8")), "announces Camunda 8");
85
+ assertEquals(warn, []);
86
+ });
87
+
88
+ test("announceEngine sends CAMUNDA_TOKEN as a bearer credential on the probe", async () => {
89
+ const { log } = capture();
90
+ let sentAuth: string | undefined;
91
+ await announceEngine(ADDR, log, {
92
+ token: "secret-token",
93
+ fetchImpl: ((_url: string, init: { headers: Record<string, string> }) => {
94
+ sentAuth = init.headers.authorization;
95
+ return Promise.resolve(okResponse({ nano: { engine: "nanobpmn" } }));
96
+ }) as unknown as typeof fetch,
97
+ });
98
+ assertEquals(sentAuth, "Bearer secret-token");
99
+ });
100
+
101
+ test("announceEngine treats 401/403 as an auth hint, not an unreachable warning", async () => {
102
+ const { log, warn } = capture();
103
+ const res = { ok: false, status: 401, json: () => Promise.reject(new Error("unused")) } as unknown as Response;
104
+ await announceEngine(ADDR, log, { fetchImpl: (() => Promise.resolve(res)) as unknown as typeof fetch });
105
+ assert(warn.some((l) => l.includes("HTTP 401") && l.includes("CAMUNDA_TOKEN")), "points at the token, not unreachability");
106
+ assert(!warn.some((l) => l.includes("could not reach")), "does not claim the engine is unreachable");
107
+ });
108
+
109
+ test("announceEngine warns (does not throw) on a non-200 response", async () => {
110
+ const { log, warn } = capture();
111
+ const res = { ok: false, status: 503, json: () => Promise.reject(new Error("unused")) } as unknown as Response;
112
+ await announceEngine(ADDR, log, { fetchImpl: (() => Promise.resolve(res)) as unknown as typeof fetch });
113
+ assert(warn.some((l) => l.includes("HTTP 503")), "warns with the status code");
114
+ });
115
+
116
+ test("announceEngine warns (does not throw) when the engine is unreachable", async () => {
117
+ const { log, warn } = capture();
118
+ await announceEngine(ADDR, log, {
119
+ fetchImpl: (() => Promise.reject(new Error("ECONNREFUSED"))) as unknown as typeof fetch,
120
+ });
121
+ assert(warn.some((l) => l.includes("could not reach") && l.includes("ECONNREFUSED")), "warns with the reason");
122
+ });
@@ -0,0 +1,134 @@
1
+ // Engine address resolution + a lightweight, non-rejecting startup preflight
2
+ // (nano-workforce#391).
3
+ //
4
+ // Two jobs, one source of truth:
5
+ // 1. `resolveEngineAddress` is the CANONICAL resolver for the engine REST
6
+ // address. Both the engine client (`main.ts`) and the demand reader
7
+ // (`app/agentic/vocab/demand-report.ts`) derive from it, so the precedence
8
+ // lives in exactly one place instead of drifting across call sites.
9
+ // 2. `announceEngine` probes `/v2/topology` at boot and reports which engine
10
+ // answered. It is DELIBERATELY informational: Nano Workforce is a
11
+ // first-class Camunda 8 client, so a non-Nano engine is announced, never
12
+ // rejected. It never throws — a boot preflight must not gate startup.
13
+ //
14
+ // Why: without this, pointing the app at the wrong address (classically another
15
+ // service already on :8080) surfaces only later as a cryptic mid-run engine
16
+ // error. Echoing the resolved address + which engine answered makes the
17
+ // misconfiguration obvious at boot.
18
+
19
+ import { envVar } from "./version.ts";
20
+
21
+ /** The resolved engine REST address plus a human label for where it came from. */
22
+ export interface EngineAddress {
23
+ /** The `/v2` REST base the engine client and demand reader talk to. */
24
+ restAddress: string;
25
+ /** Which input produced `restAddress`, for a legible startup line. */
26
+ source: string;
27
+ }
28
+
29
+ /**
30
+ * Canonical engine REST address resolution — the single source of truth.
31
+ *
32
+ * Precedence: an explicit `CAMUNDA_REST_ADDRESS` (already the `/v2` REST
33
+ * address) wins; otherwise the address is derived from `NANOBPMN_BASE_URL`
34
+ * (+ `/v2`), defaulting the base to `http://localhost:8080`. Trailing slashes
35
+ * are stripped from both inputs.
36
+ *
37
+ * `read` is injectable so the resolution is testable without mutating
38
+ * `process.env`; it defaults to the app's `envVar` (ADR 0004) accessor.
39
+ */
40
+ export function resolveEngineAddress(
41
+ read: (name: string) => string | null = envVar,
42
+ ): EngineAddress {
43
+ const explicit = read("CAMUNDA_REST_ADDRESS")?.replace(/\/+$/, "");
44
+ if (explicit) return { restAddress: explicit, source: "CAMUNDA_REST_ADDRESS" };
45
+ const base = read("NANOBPMN_BASE_URL");
46
+ const normalized = (base ?? "http://localhost:8080").replace(/\/+$/, "");
47
+ return {
48
+ restAddress: `${normalized}/v2`,
49
+ source: base ? "NANOBPMN_BASE_URL" : "default (http://localhost:8080)",
50
+ };
51
+ }
52
+
53
+ /**
54
+ * The subset of a `/v2/topology` body we read. A nanobpmn gateway advertises a
55
+ * `nano` object (its own extension) so a single call distinguishes it from a
56
+ * stock Camunda 8 gateway, which returns the same shape without it.
57
+ */
58
+ export interface TopologyProbe {
59
+ nano?: { engine?: string; version?: string; falconPath?: string } | null;
60
+ gatewayVersion?: string;
61
+ }
62
+
63
+ /**
64
+ * A human-readable identity line for whatever answered `/v2/topology`. Never
65
+ * rejects: a Camunda 8 gateway (no `nano` marker) is a supported target, so it
66
+ * is described, not refused.
67
+ */
68
+ export function describeEngine(body: TopologyProbe | null | undefined): string {
69
+ const nano = body?.nano;
70
+ if (nano?.engine) {
71
+ const version = nano.version ? ` v${nano.version}` : "";
72
+ const falcon = nano.falconPath ?? "/falcon";
73
+ return `Nano engine (${nano.engine}${version}) — Falcon streaming at ${falcon}`;
74
+ }
75
+ const gateway = body?.gatewayVersion ? ` (gateway v${body.gatewayVersion})` : "";
76
+ return `Camunda 8${gateway} — REST only (Nano Falcon streaming unavailable)`;
77
+ }
78
+
79
+ /** The logging surface `announceEngine` needs (structurally a `Logger`). */
80
+ export interface PreflightLog {
81
+ info(msg: string): void;
82
+ warn(msg: string): void;
83
+ }
84
+
85
+ /**
86
+ * Log the resolved engine address (and its source), then probe `/v2/topology`
87
+ * and announce which engine answered. Informational only — it swallows every
88
+ * failure into a `warn` and never throws, so a slow or absent engine cannot
89
+ * block boot. A missing engine is a `warn` (features will fail to start until
90
+ * it is reachable), not a fatal error.
91
+ *
92
+ * `opts.token` (the same `CAMUNDA_TOKEN` the engine client uses) is sent as a
93
+ * bearer credential so a secured cluster does not answer the probe with a
94
+ * misleading 401/403 while the real client is correctly configured.
95
+ */
96
+ export async function announceEngine(
97
+ addr: EngineAddress,
98
+ log: PreflightLog,
99
+ opts: { token?: string; fetchImpl?: typeof fetch } = {},
100
+ ): Promise<void> {
101
+ const fetchImpl = opts.fetchImpl ?? fetch;
102
+ log.info(`Engine address: ${addr.restAddress} (from ${addr.source})`);
103
+ const url = `${addr.restAddress.replace(/\/+$/, "")}/topology`;
104
+ const headers: Record<string, string> = { accept: "application/json" };
105
+ if (opts.token) headers.authorization = `Bearer ${opts.token}`;
106
+ try {
107
+ const res = await fetchImpl(url, { headers, signal: AbortSignal.timeout(3000) });
108
+ if (!res.ok) {
109
+ if (res.status === 401 || res.status === 403) {
110
+ // The engine is reachable but rejected the probe's credentials — the
111
+ // real client may still work (it authenticates independently), so this
112
+ // is an auth hint, not an "unreachable" warning.
113
+ log.warn(
114
+ `Engine preflight: ${url} returned HTTP ${res.status} (authentication). ` +
115
+ `If the engine requires a token, set CAMUNDA_TOKEN — the engine client uses it independently.`,
116
+ );
117
+ return;
118
+ }
119
+ log.warn(
120
+ `Engine preflight: ${url} returned HTTP ${res.status}. ` +
121
+ `Check CAMUNDA_REST_ADDRESS / NANOBPMN_BASE_URL — features will fail to start until the engine is reachable.`,
122
+ );
123
+ return;
124
+ }
125
+ const body: TopologyProbe = await res.json();
126
+ log.info(`Engine: ${describeEngine(body)} at ${addr.restAddress}.`);
127
+ } catch (err) {
128
+ const reason = err instanceof Error ? err.message : String(err);
129
+ log.warn(
130
+ `Engine preflight: could not reach ${url} (${reason}). ` +
131
+ `Check CAMUNDA_REST_ADDRESS / NANOBPMN_BASE_URL — features will fail to start until the engine is reachable.`,
132
+ );
133
+ }
134
+ }
package/main.ts CHANGED
@@ -20,6 +20,7 @@
20
20
  import { Server } from "node:http";
21
21
  import { createNanoSdkEngineClient, runFromEnv, selectHost } from "@nanobpm/urban";
22
22
  import { type AgenticChannelHandle, mountAgenticChannel } from "./app/agentic/channel.ts";
23
+ import { announceEngine, resolveEngineAddress } from "./app/enginePreflight.ts";
23
24
  import { MAX_ROUNDS, pollOnce } from "./app/service.ts";
24
25
  import { envVar } from "./app/version.ts";
25
26
 
@@ -29,12 +30,18 @@ const GITHUB_TOKEN = process.env.GITHUB_TOKEN ?? "";
29
30
 
30
31
  const host = selectHost();
31
32
 
32
- // One engine client, shared by the runtime (surfaces/actions/workers) and the poller. Honour
33
- // the app's documented NANOBPMN_BASE_URL as well as the runtime's CAMUNDA_REST_ADDRESS.
34
- const restAddress = process.env.CAMUNDA_REST_ADDRESS ??
35
- `${(process.env.NANOBPMN_BASE_URL ?? "http://localhost:8080").replace(/\/+$/, "")}/v2`;
33
+ // One engine client, shared by the runtime (surfaces/actions/workers) and the poller. The address
34
+ // resolution (CAMUNDA_REST_ADDRESS wins, else NANOBPMN_BASE_URL + /v2, else localhost:8080) is the
35
+ // canonical `resolveEngineAddress`, shared with the demand reader. A non-rejecting startup preflight
36
+ // then echoes the resolved address and announces which engine answered (nano-workforce#391) so a
37
+ // misconfigured address is obvious at boot rather than as a cryptic mid-run engine error.
38
+ const engineAddress = resolveEngineAddress();
39
+ await announceEngine(engineAddress, {
40
+ info: (msg) => host.log("info", msg),
41
+ warn: (msg) => host.log("warn", msg),
42
+ }, { token: process.env.CAMUNDA_TOKEN });
36
43
  const engine = await createNanoSdkEngineClient({
37
- restAddress,
44
+ restAddress: engineAddress.restAddress,
38
45
  token: process.env.CAMUNDA_TOKEN,
39
46
  transport: process.env.CAMUNDA_TRANSPORT ?? "auto",
40
47
  log: host.log,
@@ -96,7 +103,7 @@ let shuttingDown = false;
96
103
  let pollTimer: ReturnType<typeof setTimeout> | null = null;
97
104
  async function pollLoop(): Promise<void> {
98
105
  try {
99
- if (app.data) await pollOnce(app.data, engine, GITHUB_TOKEN, { restAddress, token: process.env.CAMUNDA_TOKEN });
106
+ if (app.data) await pollOnce(app.data, engine, GITHUB_TOKEN, { restAddress: engineAddress.restAddress, token: process.env.CAMUNDA_TOKEN });
100
107
  } catch (err) {
101
108
  console.error("poll error:", err);
102
109
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/nano-workforce",
3
- "version": "0.114.1",
3
+ "version": "0.115.0",
4
4
  "description": "Nano Workforce — an Agent Graph Orchestration application for Agentic SDLC: durable BPMN processes that coordinate a graph of AI agents across the software delivery lifecycle.",
5
5
  "type": "module",
6
6
  "main": "main.ts",