c8ctl-plugin-nano 1.61.0 → 1.61.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.
- package/README.md +42 -6
- package/agent-instance.mjs +166 -11
- package/c8ctl-plugin.js +1198 -38
- package/package.json +8 -8
- package/work-relay.mjs +73 -5
package/README.md
CHANGED
|
@@ -728,7 +728,7 @@ only `latest`, so the key is the reproducibility handle).
|
|
|
728
728
|
|
|
729
729
|
On completion the plugin writes an **output envelope** back under
|
|
730
730
|
`io.nanobpm.agentResult` (`{schemaVersion, status, sandbox, image, output, truncated, stderrTruncated, exitCode, signal, error, promptResourceKey?}`). When a repository was
|
|
731
|
-
provisioned (below) it also carries `{repository, branch, baseSha, headSha, commits[], pushed, pushError?, gitError?, pr?}`.
|
|
731
|
+
provisioned (below) it also carries `{repository, branch, baseSha, headSha, commits[], pushed, pushError?, pushFailed?, strandedCommits?, branchMismatch?, scanError?, gitError?, pr?}`. `pushFailed` is the explicit "push failed" flag — set for a non-zero `git push` **whose result could not be confirmed as landed at the remote** (a non-fast-forward rejection, or an auth, hook, or network error), not only a server rejection. **`pushFailed` does not by itself prove a remote push was attempted or rejected:** `finalizeGit` also sets it (with `strandedCommits` and a `branchMismatch` `{expected, actual}`) when it *refuses to push at all* because the harness moved HEAD off the provisioned work branch — or left commits abandoned on another local branch — so pushing the work branch would publish stale work and strand those commits (no `git push` runs, so `pushError` is absent in that case). As a guard against false strands, a non-zero push is re-checked with `ls-remote`: if `origin/<branch>` already points at `headSha` — or the remote tip is a **descendant** of `headSha` (another actor pushed a further commit after ours landed) — the push is treated as a transport hiccup that landed after the ref was accepted (`pushed: true`, no `pushFailed`/`pushError`); otherwise `pushFailed` is set and `strandedCommits` lists the SHAs of the new commits left UNPUSHED in the throwaway workspace — together they are the recovery handle for a failed push, so consumers must not treat `pushed: false` alone as the only signal. On such a failure (a rejected push **or** a branch-mismatch refusal) the throwaway workspace is preserved **best-effort** (even under the default `--keep-runs=false`) so those SHAs stay recoverable, but this is a *recovery window, not a durable archive*: the run-directory reaper still ages it out by mtime and worker shutdown removes the namespace — copy the stranded commits out promptly (or run with `--keep-runs`). **`scanError`** flags a **best-effort/PARTIAL** `strandedCommits` list: a commit or remote-reachability scan did **not** complete, so a consumer should recover the whole preserved workspace rather than trust the list exactly. It arises two ways: (a) a **pre-push** commit-enumeration scan failed, so `finalizeGit` refused to push on an incomplete graph — **no push was attempted** (`pushError` is absent) and `strandedCommits` is a partial surfacing; or (b) a push was **ATTEMPTED and rejected** but the remote-reachability FILTER that trims already-published commits from the strand set itself failed — so `scanError` is present **alongside** `pushError`, marking the rejected-push strand list as best-effort (it may falsely include an already-landed commit). Either way `pushFailed` stays authoritative; `scanError` is how a consumer knows the strand list is inexact (recover the whole workspace) versus an exact list (a rejected push with no `scanError`).
|
|
732
732
|
|
|
733
733
|
**Git provisioning (host).** When `--sandbox none` (the default) and the envelope
|
|
734
734
|
carries a `repository.url`, the plugin provisions a workspace on the host around
|
|
@@ -764,7 +764,17 @@ the harness:
|
|
|
764
764
|
per envelope (default 120s, or the `--clone-timeout` worker flag) as a backstop
|
|
765
765
|
for repos big enough to approach the cap even when shallow; a timeout is now
|
|
766
766
|
reported *as a timeout* rather than an opaque `exit 128`;
|
|
767
|
-
3. create `branch.create` (if set) off that target
|
|
767
|
+
3. create `branch.create` (if set) off that target — or, when `branch.create` is
|
|
768
|
+
**absent** (or names the effective base itself) **and push is enabled**, cut a
|
|
769
|
+
generated fallback work branch `nano/agent-work/<base>-<runId>` so commits are
|
|
770
|
+
never made directly on the base branch; the branch actually used (configured or
|
|
771
|
+
generated) rides back in the result envelope (`branch`) and is exported to the
|
|
772
|
+
harness as `AGENT_REPO_BRANCH`. **Exception — a detached tag/SHA checkout with
|
|
773
|
+
no `branch.create`:** with no symbolic branch to name a fallback from, it stays
|
|
774
|
+
`branch: null` and its commits are **not** pushed even when push is enabled —
|
|
775
|
+
this holds for *any* detached no-`branch.create` checkout, **including** when a
|
|
776
|
+
`branch.base` is configured (the base name is not used to synthesize a fallback
|
|
777
|
+
here). Supply `branch.create` to publish work committed off a tag/SHA base;
|
|
768
778
|
4. set a **committer identity** on the workspace, preferring the operator's own
|
|
769
779
|
(`GIT_AUTHOR_*` env → global `git config user.name/email` → the
|
|
770
780
|
`gh`-authenticated GitHub user), and only falling back to `nano-agent` when
|
|
@@ -793,10 +803,36 @@ config is neutralized (`GIT_CONFIG_GLOBAL` → the platform null device,
|
|
|
793
803
|
or `url.*.insteadOf` can't silently inject operator credentials. (An **SSH**
|
|
794
804
|
remote — `git@…`/`ssh://…` — can still authenticate via the host's SSH
|
|
795
805
|
agent/config; use HTTPS URLs if you need a guaranteed-anonymous clone.)
|
|
796
|
-
Token-backed jobs keep global config (e.g. `http.proxy`).
|
|
797
|
-
|
|
798
|
-
the merge; a clone/checkout failure
|
|
799
|
-
|
|
806
|
+
Token-backed jobs keep global config (e.g. `http.proxy`). **`pushFailed` is the
|
|
807
|
+
authoritative "work is stranded, workspace preserved" flag** (the job still
|
|
808
|
+
completes, so a later BPMN step can drive the merge); a clone/checkout failure
|
|
809
|
+
sheds the job (retryable). `pushError` is present **only** for a push that was
|
|
810
|
+
ATTEMPTED and rejected/unconfirmed — when `finalizeGit` refuses to push at all (a
|
|
811
|
+
branch mismatch, or a partial/incomplete commit scan), it sets `pushFailed`
|
|
812
|
+
(with `branchMismatch` or `scanError`) but **no** `pushError`, so consumers must
|
|
813
|
+
key recovery off `pushFailed`, not `pushError`. **`scanError` flags a
|
|
814
|
+
best-effort/PARTIAL `strandedCommits` list** — a commit/reachability scan did not
|
|
815
|
+
complete, so the list may omit or (after a rejected push) falsely include commits
|
|
816
|
+
and a consumer should recover the whole preserved workspace rather than trust the
|
|
817
|
+
list exactly. It arises two ways: (a) a pre-push commit-enumeration scan failed, so
|
|
818
|
+
`finalizeGit` refused to push on an incomplete graph (**no** `git push` ran, so
|
|
819
|
+
`pushError` is absent); or (b) a push was ATTEMPTED and rejected but the
|
|
820
|
+
remote-reachability FILTER that trims already-published commits from the strand set
|
|
821
|
+
itself failed — so `scanError` is present **alongside** `pushError`, marking the
|
|
822
|
+
otherwise-complete rejected-push strand list as best-effort. Either way `pushFailed`
|
|
823
|
+
stays authoritative. Workspaces are
|
|
824
|
+
deleted after each job (keep them with `--keep-runs`) — **except** a job whose
|
|
825
|
+
`git push` **could not be confirmed as landed** (a non-fast-forward rejection, or
|
|
826
|
+
an auth/hook/network error whose `ls-remote` re-check did not find `origin/<branch>`
|
|
827
|
+
at or ahead of `headSha` — `pushFailed` + `pushError`), **or** one where
|
|
828
|
+
`finalizeGit` refused to push (no `git push` ran, so `pushError` is absent)
|
|
829
|
+
because HEAD moved off the provisioned work branch — or left commits on another
|
|
830
|
+
local branch or an abandoned detached HEAD reachable only via the reflog (a
|
|
831
|
+
`branchMismatch` strand) — **or** because a critical local commit scan did not
|
|
832
|
+
complete (a `scanError` strand, with a best-effort PARTIAL `strandedCommits`),
|
|
833
|
+
whose workspace is preserved best-effort so its
|
|
834
|
+
`strandedCommits` stay recoverable; that preservation is still age-gated by the
|
|
835
|
+
reaper and cleared on worker shutdown, so recover the SHAs promptly.
|
|
800
836
|
|
|
801
837
|
```bash
|
|
802
838
|
# The harness sees a cloned repo at $AGENT_WORKSPACE; branch/push/PR are handled for it.
|
package/agent-instance.mjs
CHANGED
|
@@ -28,6 +28,57 @@ const SDK_UPDATE = 'updateAgentInstance';
|
|
|
28
28
|
|
|
29
29
|
const isNonBlank = (v) => v != null && String(v).trim() !== '';
|
|
30
30
|
const isPlainObject = (v) => v != null && typeof v === 'object' && !Array.isArray(v);
|
|
31
|
+
// #229: collapse CR/LF (and other line/para separators) to a single space so a
|
|
32
|
+
// multiline engine error can't split one correlation record across several
|
|
33
|
+
// worker-log lines — that would both dilute the status/body diagnostic and let a
|
|
34
|
+
// crafted error body spoof extra log lines. Used when rendering SDK errors.
|
|
35
|
+
const oneLine = (v) => String(v).replace(/[\r\n\t\f\v\u0085\u2028\u2029]+/g, ' ');
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Extract the HTTP status + response body from an SDK rejection (#229).
|
|
39
|
+
*
|
|
40
|
+
* A create/append failure logged as an opaque `status 400` is useless — the body
|
|
41
|
+
* is what distinguishes a lease-fence rejection from a schema error from a 404.
|
|
42
|
+
* The host `@camunda8/orchestration-cluster-api` client surfaces these on a few
|
|
43
|
+
* shapes depending on the transport, so probe the common ones defensively and
|
|
44
|
+
* never throw (this runs on the best-effort logging path).
|
|
45
|
+
*
|
|
46
|
+
* @param {*} err
|
|
47
|
+
* @returns {{ status: (number|string|undefined), body: (string|undefined), message: string }}
|
|
48
|
+
*/
|
|
49
|
+
export function describeSdkError(err) {
|
|
50
|
+
if (err == null) return { status: undefined, body: undefined, message: String(err) };
|
|
51
|
+
const status =
|
|
52
|
+
err.status ??
|
|
53
|
+
err.statusCode ??
|
|
54
|
+
err?.response?.status ??
|
|
55
|
+
err?.response?.statusCode ??
|
|
56
|
+
(typeof err.code === 'number' ? err.code : undefined);
|
|
57
|
+
let body =
|
|
58
|
+
err.body ??
|
|
59
|
+
err.responseBody ??
|
|
60
|
+
err?.response?.data ??
|
|
61
|
+
err?.response?.body ??
|
|
62
|
+
undefined;
|
|
63
|
+
if (body != null && typeof body !== 'string') {
|
|
64
|
+
try {
|
|
65
|
+
body = JSON.stringify(body);
|
|
66
|
+
} catch {
|
|
67
|
+
body = String(body);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const message = err.message ? String(err.message) : String(err);
|
|
71
|
+
return { status, body, message };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** One-line rendering of {@link describeSdkError} for a log line (body capped). */
|
|
75
|
+
function formatSdkError(err) {
|
|
76
|
+
const { status, body, message } = describeSdkError(err);
|
|
77
|
+
const parts = [`status ${status ?? 'unknown'}`];
|
|
78
|
+
if (isNonBlank(body)) parts.push(`body ${oneLine(String(body).slice(0, 600))}`);
|
|
79
|
+
parts.push(`msg ${oneLine(message)}`);
|
|
80
|
+
return parts.join('; ');
|
|
81
|
+
}
|
|
31
82
|
|
|
32
83
|
/**
|
|
33
84
|
* Is this activated job an `external` (job-backed) agent job — i.e. one whose
|
|
@@ -175,6 +226,24 @@ export function createAgentInstanceProducer(opts = {}) {
|
|
|
175
226
|
const leaseToken = job?.leaseToken != null ? String(job.leaseToken) : '';
|
|
176
227
|
const elementInstanceKey = job?.elementInstanceKey != null ? String(job.elementInstanceKey) : '';
|
|
177
228
|
const elementId = job?.elementId != null ? String(job.elementId) : null;
|
|
229
|
+
// #229: cross-channel correlation. Stamp jobKey + elementInstanceKey +
|
|
230
|
+
// processInstanceKey on every producer log so the AgentInstance channel can be
|
|
231
|
+
// joined to the job / relay / git channels (today AgentInstance logs carry no
|
|
232
|
+
// processInstanceKey, so the four channels can't be reconciled).
|
|
233
|
+
const processInstanceKey = job?.processInstanceKey != null ? String(job.processInstanceKey) : '';
|
|
234
|
+
const corr = () =>
|
|
235
|
+
`job ${jobKey || '?'} eik ${elementInstanceKey || '?'} pik ${processInstanceKey || '?'}`;
|
|
236
|
+
// The lease token is a secret-ish fence token — never log it whole. `slice(-6)`
|
|
237
|
+
// would print a short/malformed lease (≤6 chars) IN FULL, so only surface a tail
|
|
238
|
+
// when the token is long enough that the tail still hides most of it; otherwise
|
|
239
|
+
// emit a safe digest (presence + length). Enough either way to tell "present"
|
|
240
|
+
// from "absent" and to correlate the activation without exposing the token.
|
|
241
|
+
const leaseNote = () =>
|
|
242
|
+
!leaseToken
|
|
243
|
+
? 'lease absent'
|
|
244
|
+
: leaseToken.length > 8
|
|
245
|
+
? `lease …${leaseToken.slice(-4)}`
|
|
246
|
+
: `lease present (len ${leaseToken.length})`;
|
|
178
247
|
|
|
179
248
|
// The producer is a no-op unless every precondition holds: a usable SDK client,
|
|
180
249
|
// an external agent job, and the ACP classifier. Any missing piece leaves the
|
|
@@ -191,6 +260,16 @@ export function createAgentInstanceProducer(opts = {}) {
|
|
|
191
260
|
let activated = false;
|
|
192
261
|
let loopIteration = 0;
|
|
193
262
|
let queue = Promise.resolve();
|
|
263
|
+
// #229 turn accounting: how many AgentHistory turns were actually appended, and
|
|
264
|
+
// when the instance was minted — so `complete()` can log "N turns over Xm" and
|
|
265
|
+
// separate "created but nothing ingested" (the 0-turns husk) from "create failed".
|
|
266
|
+
let turnsAppended = 0;
|
|
267
|
+
let activatedAt = 0;
|
|
268
|
+
// #229 first-failure elevation: the FIRST per-turn append failure (400/404) for
|
|
269
|
+
// this instance is logged at `warn` (with the SDK verb + status); repeats stay at
|
|
270
|
+
// `debug` so a persistently-rejecting instance doesn't flood the log.
|
|
271
|
+
let appendFailureLogged = false;
|
|
272
|
+
let ingestFailureLogged = false;
|
|
194
273
|
// Coalesce streamed message chunks (same messageId + role) into one turn, flushed
|
|
195
274
|
// on a role/message boundary, a tool event, or completion — the engine dedups on
|
|
196
275
|
// historyItemId (it does NOT merge), so a turn must be appended exactly once, whole.
|
|
@@ -201,10 +280,30 @@ export function createAgentInstanceProducer(opts = {}) {
|
|
|
201
280
|
const iso = () => new Date(now()).toISOString();
|
|
202
281
|
|
|
203
282
|
// Serialize an SDK call onto the queue so appends preserve order and `complete`
|
|
204
|
-
// can drain them. A rejection is
|
|
205
|
-
|
|
283
|
+
// can drain them. A rejection is best-effort (never breaks the chain), but the
|
|
284
|
+
// first append failure per instance is elevated to `warn` with the SDK verb +
|
|
285
|
+
// HTTP status (#229) — per-turn append failures were invisible at `debug`.
|
|
286
|
+
const enqueue = (fn, label = 'updateAgentInstance') => {
|
|
206
287
|
queue = queue.then(fn).catch((err) => {
|
|
207
|
-
|
|
288
|
+
const { status, message } = describeSdkError(err);
|
|
289
|
+
// The first-failure elevation is for per-turn APPEND failures ONLY. A
|
|
290
|
+
// completion status update (status→COMPLETED) rides this same queue, so if
|
|
291
|
+
// it were allowed to consume the one-shot flag it would suppress the FIRST
|
|
292
|
+
// real per-turn append failure down to `debug` — the exact regression the
|
|
293
|
+
// #229 warning exists to prevent. Gate the elevation on the append label;
|
|
294
|
+
// completion failures are diagnosed separately in `complete()` (they render
|
|
295
|
+
// a failed/unknown terminal transition), so here they only ever log at debug.
|
|
296
|
+
const isAppend = label.includes('append');
|
|
297
|
+
if (isAppend && !appendFailureLogged) {
|
|
298
|
+
appendFailureLogged = true;
|
|
299
|
+
logger?.warn?.(
|
|
300
|
+
`AgentInstance producer: ${label} failed (${corr()}) — status ${status ?? 'unknown'}: ${oneLine(message)}; further append failures for this instance stay at debug.`,
|
|
301
|
+
);
|
|
302
|
+
} else {
|
|
303
|
+
logger?.debug?.(
|
|
304
|
+
`AgentInstance producer: ${label} failed (${corr()}) — status ${status ?? 'unknown'}: ${oneLine(message)}`,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
208
307
|
});
|
|
209
308
|
return queue;
|
|
210
309
|
};
|
|
@@ -222,8 +321,16 @@ export function createAgentInstanceProducer(opts = {}) {
|
|
|
222
321
|
history: [turn],
|
|
223
322
|
};
|
|
224
323
|
if (status) req.status = status;
|
|
225
|
-
await camunda[SDK_UPDATE](req);
|
|
226
|
-
|
|
324
|
+
const res = await camunda[SDK_UPDATE](req);
|
|
325
|
+
// #229/#232: the engine dedups appends by historyItemId, so a retry or a
|
|
326
|
+
// reactivation can return 200 while creating ZERO new history entries. Count
|
|
327
|
+
// what the engine actually CREATED (`res.createdHistory`) — not the attempt —
|
|
328
|
+
// so the completion counter separates a real append from a deduplicated no-op
|
|
329
|
+
// and keeps the 0-turns husk diagnosis honest. Fall back to +1 only when the
|
|
330
|
+
// response omits the field (older engine), so a genuine append is never
|
|
331
|
+
// under-counted.
|
|
332
|
+
turnsAppended += Array.isArray(res?.createdHistory) ? res.createdHistory.length : 1;
|
|
333
|
+
}, 'updateAgentInstance(append)');
|
|
227
334
|
};
|
|
228
335
|
|
|
229
336
|
const flushMessage = () => {
|
|
@@ -289,6 +396,18 @@ export function createAgentInstanceProducer(opts = {}) {
|
|
|
289
396
|
appendTurn(turn);
|
|
290
397
|
};
|
|
291
398
|
|
|
399
|
+
// #229: first-failure elevation for ingest faults (classifier OR handler). The
|
|
400
|
+
// FIRST ingest failure per instance logs at `warn`; repeats stay at `debug` so a
|
|
401
|
+
// persistently-faulting instance doesn't flood the log.
|
|
402
|
+
const noteIngestFailure = (err) => {
|
|
403
|
+
if (!ingestFailureLogged) {
|
|
404
|
+
ingestFailureLogged = true;
|
|
405
|
+
logger?.warn?.(`AgentInstance producer: ingest failed (${corr()}) — ${oneLine(err?.message || err)}; further ingest failures for this instance stay at debug.`);
|
|
406
|
+
} else {
|
|
407
|
+
logger?.debug?.(`AgentInstance producer: ingest failed (${corr()}) — ${oneLine(err?.message || err)}`);
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
|
|
292
411
|
return {
|
|
293
412
|
/** True once the AgentInstance has been minted (or is being minted). */
|
|
294
413
|
get active() {
|
|
@@ -339,15 +458,22 @@ export function createAgentInstanceProducer(opts = {}) {
|
|
|
339
458
|
// result carries no key, fall back to the elementInstanceKey correlation is
|
|
340
459
|
// not possible for updates (they need the agentInstanceKey), so disable.
|
|
341
460
|
disabled = true;
|
|
342
|
-
logger?.warn?.(
|
|
461
|
+
logger?.warn?.(`AgentInstance producer: create returned no agentInstanceKey (${corr()}); disabling durable transcript for this job.`);
|
|
343
462
|
return false;
|
|
344
463
|
}
|
|
345
464
|
loopIteration = 1;
|
|
346
|
-
|
|
465
|
+
activatedAt = now();
|
|
466
|
+
logger?.info?.(`AgentInstance ${agentInstanceKey} minted (${corr()}; ${leaseNote()}; model ${oneLine(def.model)}/${oneLine(def.provider)}).`);
|
|
347
467
|
return true;
|
|
348
468
|
} catch (err) {
|
|
349
469
|
disabled = true;
|
|
350
|
-
|
|
470
|
+
// #229: the single line that would have root-caused the 20974 work-loss.
|
|
471
|
+
// Log the engine's HTTP status + response body (a lease-fence 400 vs a
|
|
472
|
+
// schema 400 vs a 404) plus the request correlation actually sent —
|
|
473
|
+
// elementInstanceKey/jobKey/processInstanceKey, whether the lease was
|
|
474
|
+
// present + its tail, and the model/provider. An opaque "status 400" alone
|
|
475
|
+
// is useless.
|
|
476
|
+
logger?.warn?.(`AgentInstance producer: createAgentInstance REJECTED (${corr()}; ${leaseNote()}; model ${oneLine(def.model)}/${oneLine(def.provider)}) — ${formatSdkError(err)}; continuing without a durable transcript (job completion unaffected).`);
|
|
351
477
|
return false;
|
|
352
478
|
}
|
|
353
479
|
},
|
|
@@ -362,7 +488,11 @@ export function createAgentInstanceProducer(opts = {}) {
|
|
|
362
488
|
let classified;
|
|
363
489
|
try {
|
|
364
490
|
classified = classify(rawUpdate);
|
|
365
|
-
} catch {
|
|
491
|
+
} catch (err) {
|
|
492
|
+
// A classifier/translation fault is an ingest failure too — route it
|
|
493
|
+
// through the same first-failure elevation (#229) instead of returning
|
|
494
|
+
// silently, or the ingest path can still drop every turn with no warning.
|
|
495
|
+
noteIngestFailure(err);
|
|
366
496
|
return;
|
|
367
497
|
}
|
|
368
498
|
if (!classified || typeof classified !== 'object') return;
|
|
@@ -401,7 +531,10 @@ export function createAgentInstanceProducer(opts = {}) {
|
|
|
401
531
|
break;
|
|
402
532
|
}
|
|
403
533
|
} catch (err) {
|
|
404
|
-
|
|
534
|
+
// #229: elevate the FIRST ingest failure per instance to `warn` (repeats
|
|
535
|
+
// stay `debug`) so a translation/append fault that silently drops every
|
|
536
|
+
// turn is visible at normal verbosity.
|
|
537
|
+
noteIngestFailure(err);
|
|
405
538
|
}
|
|
406
539
|
},
|
|
407
540
|
|
|
@@ -425,6 +558,12 @@ export function createAgentInstanceProducer(opts = {}) {
|
|
|
425
558
|
return;
|
|
426
559
|
}
|
|
427
560
|
flushMessage();
|
|
561
|
+
// Track whether the COMPLETED status request actually resolved. The update
|
|
562
|
+
// rides the best-effort queue (whose catch swallows SDK rejections), so we
|
|
563
|
+
// must NOT infer the terminal transition from `ok` alone — a 400/404 on the
|
|
564
|
+
// status update would otherwise be logged as a successful COMPLETED (#229),
|
|
565
|
+
// defeating the husk diagnosis. `null` ⇒ no status update attempted.
|
|
566
|
+
let statusResolved = ok ? false : null;
|
|
428
567
|
if (ok) {
|
|
429
568
|
enqueue(async () => {
|
|
430
569
|
await camunda[SDK_UPDATE]({
|
|
@@ -434,9 +573,25 @@ export function createAgentInstanceProducer(opts = {}) {
|
|
|
434
573
|
jobLease: leaseToken,
|
|
435
574
|
status: 'COMPLETED',
|
|
436
575
|
});
|
|
437
|
-
|
|
576
|
+
statusResolved = true;
|
|
577
|
+
}, 'updateAgentInstance(status→COMPLETED)');
|
|
438
578
|
}
|
|
439
579
|
try { await queue; } catch { /* best effort */ }
|
|
580
|
+
// #229: log a turn counter — "N turns appended over Xm, status→…" — so the
|
|
581
|
+
// 0-turns husk ("created but nothing ingested") is distinguishable from a
|
|
582
|
+
// healthy run at a glance, separate from the "create failed" line above.
|
|
583
|
+
const elapsedMs = activatedAt ? Math.max(0, now() - activatedAt) : 0;
|
|
584
|
+
const mins = (elapsedMs / 60000).toFixed(1);
|
|
585
|
+
// Render the ACTUAL terminal transition: COMPLETED only when the status
|
|
586
|
+
// update resolved; on a rejected update say so (the instance stays
|
|
587
|
+
// non-terminal, so a retry/reactivation still continues it); on a failed run
|
|
588
|
+
// no update was attempted at all.
|
|
589
|
+
const transition = ok
|
|
590
|
+
? (statusResolved
|
|
591
|
+
? 'COMPLETED'
|
|
592
|
+
: 'COMPLETED update FAILED — left non-terminal (retry/reactivation continues it)')
|
|
593
|
+
: 'left non-terminal (retry/reactivation continues it)';
|
|
594
|
+
logger?.info?.(`AgentInstance ${agentInstanceKey} (${corr()}): ${turnsAppended} turn(s) appended over ${mins}m, status→${transition}.`);
|
|
440
595
|
},
|
|
441
596
|
};
|
|
442
597
|
}
|