c8ctl-plugin-nano 1.58.0 → 1.59.1

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.
@@ -34,9 +34,10 @@ const isPlainObject = (v) => v != null && typeof v === 'object' && !Array.isArra
34
34
  * element carries `zeebe:agentDefinition agentType="external"`?
35
35
  *
36
36
  * The engine surfaces exactly this eligibility as the pair of fields it stamps on
37
- * an external agent job's activation: an opaque per-activation `jobLease` token
38
- * (distinct from the job's `deadline`; nanobpmn #1106) plus the `elementInstanceKey`
39
- * the AgentInstance correlates on. The engine-native `aiAgentTask`/`aiAgentSubProcess`
37
+ * an external agent job's activation: an opaque per-activation lease token
38
+ * `leaseToken` on the activated job (Camunda v10 ActivatedJobResult; distinct from
39
+ * the job's `deadline`, nanobpmn #1106) plus the `elementInstanceKey` the
40
+ * AgentInstance correlates on. The engine-native `aiAgentTask`/`aiAgentSubProcess`
40
41
  * variants auto-mint their AgentInstance and never create an activatable job, so any
41
42
  * job a worker actually activates that carries a lease token IS an external agent
42
43
  * job. Absence of either field means "not an external agent job" → the producer
@@ -44,7 +45,7 @@ const isPlainObject = (v) => v != null && typeof v === 'object' && !Array.isArra
44
45
  */
45
46
  export function isExternalAgentJob(job) {
46
47
  if (!isPlainObject(job)) return false;
47
- return isNonBlank(job.jobLease) && isNonBlank(job.elementInstanceKey);
48
+ return isNonBlank(job.leaseToken) && isNonBlank(job.elementInstanceKey);
48
49
  }
49
50
 
50
51
  /** Best-effort provider inference from a model identifier (openai/anthropic/…). */
@@ -145,7 +146,7 @@ function contentForResult(result) {
145
146
  *
146
147
  * @param {object} opts
147
148
  * @param {object} opts.camunda Host SDK client (createAgentInstance/updateAgentInstance).
148
- * @param {object} opts.job The activated job (jobKey/jobLease/elementInstanceKey/elementId).
149
+ * @param {object} opts.job The activated job (jobKey/leaseToken/elementInstanceKey/elementId).
149
150
  * @param {object} [opts.profile] The worker profile (model/provider seed the definition).
150
151
  * @param {object} [opts.envelope] The normalized task envelope (task.prompt → systemPrompt).
151
152
  * @param {object} [opts.logger] Output-mode-aware logger (warn/info/debug).
@@ -166,7 +167,12 @@ export function createAgentInstanceProducer(opts = {}) {
166
167
  const classify = typeof sessionAcp?.classifyUpdate === 'function' ? sessionAcp.classifyUpdate : null;
167
168
 
168
169
  const jobKey = job?.jobKey != null ? String(job.jobKey) : '';
169
- const jobLease = job?.jobLease != null ? String(job.jobLease) : '';
170
+ // The activation lease is carried on the job as `leaseToken` (Camunda v10
171
+ // ActivatedJobResult). It is submitted BACK to createAgentInstance/updateAgentInstance
172
+ // as the request-body field `jobLease` (agent-instances.yaml) — the same opaque token,
173
+ // named differently on the two sides of the contract. One internal name here; the
174
+ // translation to `jobLease` happens only at each SDK call below.
175
+ const leaseToken = job?.leaseToken != null ? String(job.leaseToken) : '';
170
176
  const elementInstanceKey = job?.elementInstanceKey != null ? String(job.elementInstanceKey) : '';
171
177
  const elementId = job?.elementId != null ? String(job.elementId) : null;
172
178
 
@@ -212,7 +218,7 @@ export function createAgentInstanceProducer(opts = {}) {
212
218
  agentInstanceKey,
213
219
  elementInstanceKey,
214
220
  jobKey,
215
- jobLease,
221
+ jobLease: leaseToken,
216
222
  history: [turn],
217
223
  };
218
224
  if (status) req.status = status;
@@ -321,7 +327,7 @@ export function createAgentInstanceProducer(opts = {}) {
321
327
  const res = await camunda[SDK_CREATE]({
322
328
  elementInstanceKey,
323
329
  jobKey,
324
- jobLease,
330
+ jobLease: leaseToken,
325
331
  history: [configTurn],
326
332
  });
327
333
  agentInstanceKey =
@@ -425,7 +431,7 @@ export function createAgentInstanceProducer(opts = {}) {
425
431
  agentInstanceKey,
426
432
  elementInstanceKey,
427
433
  jobKey,
428
- jobLease,
434
+ jobLease: leaseToken,
429
435
  status: 'COMPLETED',
430
436
  });
431
437
  });
@@ -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 `/agentic`)
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
- * @returns {Promise<() => import('./supervisor.dist.js').RawEmitClient>} a synchronous `connect` factory
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 } = opts || {};
321
- if (typeof url !== 'string' || url.trim() === '') {
322
- throw new Error('createRawEmitConnect requires an agentic channel base url');
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
- return () =>
329
- buildRawEmitClient({
330
- channelUrl,
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 agenticTarget = await resolveAgenticTarget({ camunda, logger });
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
- agenticState = agenticStateForTarget(agenticTarget);
7858
- logger.info(` agentic channel: ${agenticTarget.message}`);
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`, so the
7889
- // retired #133 self-heal loop and #144/#147 liveness watchdog are gone the
7890
- // marker's agentic status is driven by the endpoint's connection observer below.
7891
- // Constructing the endpoint opens NO socket (the runtime's supervision calls the
7892
- // connect factory), so this stays cheap and side-effect-free until `run` forks.
7893
- if (agenticCfg) {
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
- url: agenticCfg.url,
7897
- token: agenticCfg.token,
7898
- credential: agenticCfg.credential,
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.58.0",
3
+ "version": "1.59.1",
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.58.0",
77
- "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.58.0",
78
- "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.58.0",
79
- "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.58.0",
80
- "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.58.0",
81
- "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.58.0",
82
- "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.58.0"
76
+ "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.59.1",
77
+ "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.59.1",
78
+ "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.59.1",
79
+ "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.59.1",
80
+ "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.59.1",
81
+ "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.59.1",
82
+ "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.59.1"
83
83
  }
84
84
  }
@@ -141,17 +141,22 @@ function mapJob(raw) {
141
141
  const pik = raw.processInstanceKey;
142
142
  if (pik !== undefined && pik !== null) job.processInstanceKey = String(pik);
143
143
  // Engine-native AgentInstance attribution (issue #194): the element instance the
144
- // AgentInstance correlates on, the element id, and the opaque per-activation
145
- // `jobLease` token that lease-gates a `createAgentInstance` for an `external`
146
- // agent job (nanobpmn #1099/#1106). These ride opaquely to the runner exactly as
147
- // the SDK activation surfaces them; absent for ordinary (non-agent) jobs, in which
148
- // case the durable-transcript producer stays inert.
144
+ // AgentInstance correlates on, the element id, and the opaque per-activation lease
145
+ // token that lease-gates a `createAgentInstance` for an `external` agent job
146
+ // (nanobpmn #1099/#1106). The Camunda v10 REST contract carries the lease on the
147
+ // ACTIVATED JOB as `leaseToken` (jobs.yaml ActivatedJobResult) NOT `jobLease`,
148
+ // which is the field name only on the createAgentInstance/updateAgentInstance REQUEST
149
+ // body (agent-instances.yaml). The harness carries ONE internal name — `leaseToken` —
150
+ // all the way through, and translates to `jobLease` only at that SDK body (see
151
+ // agent-instance.mjs). These ride opaquely to the runner exactly as the SDK activation
152
+ // surfaces them; absent for ordinary (non-agent) jobs, in which case the
153
+ // durable-transcript producer stays inert.
149
154
  const eik = raw.elementInstanceKey;
150
155
  if (eik !== undefined && eik !== null) job.elementInstanceKey = String(eik);
151
156
  const eid = raw.elementId;
152
157
  if (eid !== undefined && eid !== null) job.elementId = String(eid);
153
- const lease = raw.jobLease;
154
- if (lease !== undefined && lease !== null) job.jobLease = String(lease);
158
+ const lease = raw.leaseToken;
159
+ if (lease !== undefined && lease !== null) job.leaseToken = String(lease);
155
160
  return job;
156
161
  }
157
162
 
@@ -254,6 +259,12 @@ export function createRawEngineClient(opts = {}) {
254
259
  maxJobsToActivate: req.maxJobsToActivate,
255
260
  timeout: req.lockMs,
256
261
  requestTimeout: req.requestTimeoutMs,
262
+ // Request a per-activation lease (Camunda v10 `withLease`): the engine then
263
+ // stamps each activated job with a distinct opaque `leaseToken`
264
+ // (ActivatedJobResult.leaseToken) which lease-gates the external agent job's
265
+ // `createAgentInstance` (nanobpmn #1099/#1106). Without it the token is absent
266
+ // and the durable AgentInstance producer stays inert.
267
+ withLease: true,
257
268
  };
258
269
  // Give the abort budget slack over the server long-poll so we don't cancel
259
270
  // a still-valid long-poll a hair before the server would answer it.