@themoltnet/node-red-contrib-core 0.4.0 → 0.7.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.
@@ -0,0 +1,149 @@
1
+ import { a as nonEmpty, c as positiveInt } from "./query-utils.js";
2
+ import { Readable } from "node:stream";
3
+ import { Buffer } from "node:buffer";
4
+ function payloadRecord(msg) {
5
+ if (!msg.payload || typeof msg.payload !== "object") return {};
6
+ if (Buffer.isBuffer(msg.payload)) return {};
7
+ return msg.payload;
8
+ }
9
+ function resolveTaskId(msg, configured) {
10
+ if (typeof msg.taskId === "string" && msg.taskId) return msg.taskId;
11
+ const payload = payloadRecord(msg);
12
+ return nonEmpty(payload.taskId) ?? nonEmpty(payload.id) ?? nonEmpty(recordField(payload.task, "id")) ?? nonEmpty(configured);
13
+ }
14
+ function resolveTeamId(msg, configured, agentNode, allowMsgTeamOverride) {
15
+ if (!allowMsgTeamOverride) return nonEmpty(configured) ?? agentNode.teamId;
16
+ if (typeof msg.teamId === "string" && msg.teamId) return msg.teamId;
17
+ return nonEmpty(payloadRecord(msg).teamId) ?? nonEmpty(configured) ?? agentNode.teamId;
18
+ }
19
+ function resolveAttemptN(msg, configured) {
20
+ const payload = payloadRecord(msg);
21
+ return positiveInt(msg.attemptN) ?? positiveInt(payload.attemptN) ?? positiveInt(recordField(payload.attempt, "attemptN")) ?? positiveInt(recordField(payload.artifact, "attemptN")) ?? positiveInt(configured);
22
+ }
23
+ function requireArtifactContext(nodeName, msg, configuredTaskId, configuredTeamId, agentNode, allowMsgTeamOverride) {
24
+ const taskId = resolveTaskId(msg, configuredTaskId);
25
+ if (!taskId) throw new Error(`${nodeName}: taskId is required`);
26
+ const teamId = resolveTeamId(msg, configuredTeamId, agentNode, allowMsgTeamOverride);
27
+ if (!teamId) throw new Error(`${nodeName}: teamId is required`);
28
+ return {
29
+ taskId,
30
+ teamId
31
+ };
32
+ }
33
+ function requireAttemptContext(nodeName, msg, configuredTaskId, configuredTeamId, configuredAttemptN, agentNode, allowMsgTeamOverride) {
34
+ const context = requireArtifactContext(nodeName, msg, configuredTaskId, configuredTeamId, agentNode, allowMsgTeamOverride);
35
+ const attemptN = resolveAttemptN(msg, configuredAttemptN);
36
+ if (!attemptN) throw new Error(`${nodeName}: attemptN is required`);
37
+ return {
38
+ ...context,
39
+ attemptN
40
+ };
41
+ }
42
+ function resolveField(msg, name, configured) {
43
+ return nonEmpty(payloadRecord(msg)[name]) ?? nonEmpty(configured);
44
+ }
45
+ function resolveMaxBytes(configured) {
46
+ return positiveInt(configured) ?? 26214400;
47
+ }
48
+ function resolveUploadBody(msg, maxBytes) {
49
+ const payload = msg.payload;
50
+ if (Buffer.isBuffer(payload)) return enforceMaxBytes(payload, maxBytes);
51
+ if (payload instanceof Uint8Array) return enforceMaxBytes(payload, maxBytes);
52
+ if (payload instanceof ArrayBuffer) {
53
+ if (payload.byteLength > maxBytes) throw tooLarge("upload", maxBytes);
54
+ return new Uint8Array(payload);
55
+ }
56
+ if (typeof payload === "string") {
57
+ if (Buffer.byteLength(payload) > maxBytes) throw tooLarge("upload", maxBytes);
58
+ return new TextEncoder().encode(payload);
59
+ }
60
+ const record = payloadRecord(msg);
61
+ if (typeof record.contentBase64 === "string") {
62
+ const normalized = record.contentBase64.replace(/\s/g, "");
63
+ if (decodedBase64Length(normalized) > maxBytes) throw tooLarge("upload", maxBytes);
64
+ return Buffer.from(normalized, "base64");
65
+ }
66
+ const content = record.content ?? record.body;
67
+ if (Buffer.isBuffer(content)) return enforceMaxBytes(content, maxBytes);
68
+ if (content instanceof Uint8Array) return enforceMaxBytes(content, maxBytes);
69
+ if (content instanceof ArrayBuffer) {
70
+ if (content.byteLength > maxBytes) throw tooLarge("upload", maxBytes);
71
+ return new Uint8Array(content);
72
+ }
73
+ if (typeof content === "string") {
74
+ if (Buffer.byteLength(content) > maxBytes) throw tooLarge("upload", maxBytes);
75
+ return new TextEncoder().encode(content);
76
+ }
77
+ throw new Error("task-artifact-upload: payload content is required");
78
+ }
79
+ async function collectArtifactBody(value, maxBytes) {
80
+ const source = value && typeof value === "object" && "stream" in value ? value.stream : value;
81
+ if (Buffer.isBuffer(source)) {
82
+ if (source.byteLength > maxBytes) throw tooLarge("download", maxBytes);
83
+ return source;
84
+ }
85
+ if (source instanceof Uint8Array) {
86
+ if (source.byteLength > maxBytes) throw tooLarge("download", maxBytes);
87
+ return Buffer.from(source);
88
+ }
89
+ if (source instanceof ArrayBuffer) {
90
+ if (source.byteLength > maxBytes) throw tooLarge("download", maxBytes);
91
+ return Buffer.from(source);
92
+ }
93
+ if (typeof source === "string") {
94
+ if (Buffer.byteLength(source) > maxBytes) throw tooLarge("download", maxBytes);
95
+ return Buffer.from(source);
96
+ }
97
+ if (source instanceof Readable) {
98
+ const chunks = [];
99
+ let bytes = 0;
100
+ for await (const chunk of source) bytes = pushChunk(chunks, chunk, bytes, maxBytes);
101
+ return Buffer.concat(chunks);
102
+ }
103
+ if (source && typeof source === "object" && Symbol.asyncIterator in source) {
104
+ const chunks = [];
105
+ let bytes = 0;
106
+ for await (const chunk of source) bytes = pushChunk(chunks, chunk, bytes, maxBytes);
107
+ return Buffer.concat(chunks);
108
+ }
109
+ if (source && typeof source === "object" && "arrayBuffer" in source) {
110
+ const size = source.size;
111
+ if (typeof size === "number" && size > maxBytes) throw tooLarge("download", maxBytes);
112
+ const arrayBuffer = await source.arrayBuffer();
113
+ if (arrayBuffer.byteLength > maxBytes) throw tooLarge("download", maxBytes);
114
+ return Buffer.from(arrayBuffer);
115
+ }
116
+ throw new Error("task-artifact-download: unsupported artifact body");
117
+ }
118
+ function toBuffer(value) {
119
+ if (Buffer.isBuffer(value)) return value;
120
+ if (value instanceof Uint8Array) return Buffer.from(value);
121
+ if (value instanceof ArrayBuffer) return Buffer.from(value);
122
+ if (typeof value === "string") return Buffer.from(value);
123
+ return Buffer.from(String(value));
124
+ }
125
+ function recordField(value, key) {
126
+ if (!value || typeof value !== "object") return void 0;
127
+ return value[key];
128
+ }
129
+ function enforceMaxBytes(value, maxBytes) {
130
+ if (value.byteLength > maxBytes) throw tooLarge("upload", maxBytes);
131
+ return value;
132
+ }
133
+ function pushChunk(chunks, chunk, bytes, maxBytes) {
134
+ const next = toBuffer(chunk);
135
+ const total = bytes + next.byteLength;
136
+ if (total > maxBytes) throw tooLarge("download", maxBytes);
137
+ chunks.push(next);
138
+ return total;
139
+ }
140
+ function decodedBase64Length(value) {
141
+ if (!value) return 0;
142
+ const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
143
+ return Math.floor(value.length * 3 / 4) - padding;
144
+ }
145
+ function tooLarge(operation, maxBytes) {
146
+ return /* @__PURE__ */ new Error(`task-artifact-${operation}: artifact body exceeds ${maxBytes} bytes`);
147
+ }
148
+ //#endregion
149
+ export { requireAttemptContext as a, resolveUploadBody as c, requireArtifactContext as i, payloadRecord as n, resolveField as o, recordField as r, resolveMaxBytes as s, collectArtifactBody as t };
@@ -0,0 +1,78 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('moltnet-task-artifacts-list', {
3
+ category: 'moltnet',
4
+ color: '#00d4c8',
5
+ paletteLabel: 'task artifacts: list',
6
+ defaults: {
7
+ name: { value: '' },
8
+ agent: { value: '', type: 'moltnet-agent', required: true },
9
+ taskId: { value: '' },
10
+ teamId: { value: '' },
11
+ allowMsgTeamOverride: { value: false },
12
+ limit: { value: 20, validate: RED.validators.number() },
13
+ cursor: { value: '' },
14
+ },
15
+ inputs: 1,
16
+ outputs: 1,
17
+ icon: 'font-awesome/fa-list',
18
+ label: function () {
19
+ return this.name || 'task artifacts: list';
20
+ },
21
+ });
22
+ </script>
23
+
24
+ <script type="text/html" data-template-name="moltnet-task-artifacts-list">
25
+ <div class="form-row">
26
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
27
+ <input type="text" id="node-input-name" />
28
+ </div>
29
+ <div class="form-row">
30
+ <label for="node-input-agent"><i class="fa fa-user"></i> Agent</label>
31
+ <input type="text" id="node-input-agent" />
32
+ </div>
33
+ <div class="form-row">
34
+ <label for="node-input-taskId"><i class="fa fa-hashtag"></i> Task ID</label>
35
+ <input
36
+ type="text"
37
+ id="node-input-taskId"
38
+ placeholder="msg.payload.taskId"
39
+ />
40
+ </div>
41
+ <div class="form-row">
42
+ <label for="node-input-teamId"><i class="fa fa-users"></i> Team ID</label>
43
+ <input type="text" id="node-input-teamId" placeholder="agent teamId" />
44
+ </div>
45
+ <div class="form-row">
46
+ <label for="node-input-allowMsgTeamOverride"
47
+ ><i class="fa fa-random"></i> Team override</label
48
+ >
49
+ <input
50
+ type="checkbox"
51
+ id="node-input-allowMsgTeamOverride"
52
+ style="display: inline-block; width: auto; vertical-align: top"
53
+ />
54
+ <span>Allow <code>msg.teamId</code></span>
55
+ </div>
56
+ <div class="form-row">
57
+ <label for="node-input-limit"><i class="fa fa-hashtag"></i> Limit</label>
58
+ <input type="number" id="node-input-limit" />
59
+ </div>
60
+ <div class="form-row">
61
+ <label for="node-input-cursor"><i class="fa fa-forward"></i> Cursor</label>
62
+ <input type="text" id="node-input-cursor" />
63
+ </div>
64
+ </script>
65
+
66
+ <script type="text/html" data-help-name="moltnet-task-artifacts-list">
67
+ <p>
68
+ Lists artifacts for a MoltNet task. The task id is taken from
69
+ <code>msg.taskId</code>, <code>msg.payload.taskId</code>,
70
+ <code>msg.payload.id</code>, or the configured Task ID. Team ID is taken
71
+ from this node or the configured agent. Enable Team override to allow
72
+ <code>msg.teamId</code> or <code>msg.payload.teamId</code>.
73
+ </p>
74
+ <p>
75
+ Emits artifact rows on <code>msg.payload</code>. Pagination metadata, the
76
+ final query, and the full page are on <code>msg.artifacts</code>.
77
+ </p>
78
+ </script>
@@ -0,0 +1,61 @@
1
+ import { t as withAgent } from "./agent-call.js";
2
+ import { a as nonEmpty, c as positiveInt, n as compact, t as bool } from "./query-utils.js";
3
+ import { i as requireArtifactContext, n as payloadRecord } from "./task-artifact-utils.js";
4
+ //#region src/nodes/task-artifacts-list.ts
5
+ var init = (RED) => {
6
+ function TaskArtifactsListNode(def) {
7
+ RED.nodes.createNode(this, def);
8
+ const agentNode = def.agent ? RED.nodes.getNode(def.agent) : null;
9
+ this.on("input", (msg, send, done) => {
10
+ const run = async () => {
11
+ try {
12
+ if (!agentNode || typeof agentNode.getAgent !== "function") throw new Error("task-artifacts-list: no moltnet-agent configured");
13
+ const { taskId, teamId } = requireArtifactContext("task-artifacts-list", msg, def.taskId, def.teamId, agentNode, bool(def.allowMsgTeamOverride) ?? false);
14
+ this.status({
15
+ fill: "blue",
16
+ shape: "dot",
17
+ text: "loading…"
18
+ });
19
+ const query = buildQuery(def, msg);
20
+ const page = await withAgent(agentNode, (agent) => agent.tasks.artifacts.listPage(taskId, query, { teamId }));
21
+ const out = RED.util.cloneMessage(msg);
22
+ out.payload = page.artifacts;
23
+ out.taskId = taskId;
24
+ out.artifacts = {
25
+ taskId,
26
+ teamId,
27
+ query,
28
+ count: page.artifacts.length,
29
+ nextCursor: page.nextCursor,
30
+ page
31
+ };
32
+ this.status({
33
+ fill: "green",
34
+ shape: "dot",
35
+ text: `${page.artifacts.length} artifact(s)`
36
+ });
37
+ send(out);
38
+ done();
39
+ } catch (err) {
40
+ this.status({
41
+ fill: "red",
42
+ shape: "ring",
43
+ text: "error"
44
+ });
45
+ done(err instanceof Error ? err : new Error(String(err)));
46
+ }
47
+ };
48
+ run();
49
+ });
50
+ }
51
+ RED.nodes.registerType("moltnet-task-artifacts-list", TaskArtifactsListNode);
52
+ };
53
+ function buildQuery(def, msg) {
54
+ const payload = payloadRecord(msg);
55
+ return compact({
56
+ limit: positiveInt(payload.limit) ?? positiveInt(def.limit),
57
+ cursor: nonEmpty(payload.cursor) ?? nonEmpty(def.cursor)
58
+ });
59
+ }
60
+ //#endregion
61
+ export { init as default };
@@ -1,3 +1,4 @@
1
+ import { t as withAgent } from "./agent-call.js";
1
2
  import { t as buildTaskSnapshot } from "./task-snapshot.js";
2
3
  //#region src/nodes/task-get.ts
3
4
  var init = (RED) => {
@@ -15,8 +16,7 @@ var init = (RED) => {
15
16
  shape: "dot",
16
17
  text: "loading…"
17
18
  });
18
- const agent = await agentNode.getAgent();
19
- const [task, attempts] = await Promise.all([agent.tasks.get(taskId), agent.tasks.listAttempts(taskId)]);
19
+ const [task, attempts] = await withAgent(agentNode, (agent) => Promise.all([agent.tasks.get(taskId), agent.tasks.listAttempts(taskId)]));
20
20
  const snapshot = buildTaskSnapshot(task, attempts);
21
21
  const out = RED.util.cloneMessage(msg);
22
22
  out.payload = snapshot;
@@ -1,3 +1,4 @@
1
+ import { t as withAgent } from "./agent-call.js";
1
2
  import { n as isTerminalTaskStatus, t as buildTaskSnapshot } from "./task-snapshot.js";
2
3
  //#region src/nodes/task-wait.ts
3
4
  var DEFAULT_POLL_SEC = 5;
@@ -9,48 +10,63 @@ var init = (RED) => {
9
10
  const pollMs = Math.max(1, def.pollIntervalSec || DEFAULT_POLL_SEC) * 1e3;
10
11
  const timeoutMs = (def.timeoutSec ?? DEFAULT_TIMEOUT_SEC) > 0 ? (def.timeoutSec ?? DEFAULT_TIMEOUT_SEC) * 1e3 : 0;
11
12
  const kindAllow = parseKinds(def.kinds);
13
+ const active = /* @__PURE__ */ new Map();
12
14
  const pending = /* @__PURE__ */ new Set();
13
15
  this.on("close", () => {
14
16
  for (const t of pending) clearTimeout(t);
15
17
  pending.clear();
16
18
  });
17
19
  this.on("input", (msg, send, done) => {
20
+ let taskIdForStatus;
21
+ let label = describeMessage(msg);
18
22
  const run = async () => {
19
23
  try {
20
24
  if (!agentNode || typeof agentNode.getAgent !== "function") throw new Error("task-wait: no moltnet-agent configured");
21
25
  const taskId = resolveTaskId(msg, def.taskId);
22
26
  if (!taskId) throw new Error("task-wait: taskId is required");
23
- const agent = await agentNode.getAgent();
27
+ taskIdForStatus = taskId;
28
+ label = describeWait(taskId, msg, label);
29
+ active.set(taskId, label);
30
+ const correlationId = resolveCorrelationId(msg);
24
31
  const startedAt = Date.now();
25
32
  let afterSeq;
26
33
  let polls = 0;
27
34
  this.status({
28
35
  fill: "blue",
29
36
  shape: "dot",
30
- text: "waiting"
37
+ text: statusText("waiting", label, active.size)
31
38
  });
32
39
  for (;;) {
33
- if (def.tail) afterSeq = await drainMessages({
40
+ if (def.tail) afterSeq = await withAgent(agentNode, (agent) => drainMessages({
34
41
  agent,
35
42
  taskId,
36
43
  afterSeq,
37
44
  kindAllow,
38
45
  emit: (m) => {
39
46
  const tailMsg = RED.util.cloneMessage(msg);
40
- tailMsg.payload = m;
47
+ tailMsg.payload = correlationId ? {
48
+ ...m,
49
+ correlationId
50
+ } : m;
41
51
  tailMsg.taskId = taskId;
52
+ if (correlationId) tailMsg.correlationId = correlationId;
42
53
  send([tailMsg, null]);
43
54
  }
44
- });
45
- const task = await agent.tasks.get(taskId);
55
+ }));
56
+ const task = await withAgent(agentNode, (agent) => agent.tasks.get(taskId));
46
57
  if (isTerminalTaskStatus(task.status)) {
47
- const snapshot = buildTaskSnapshot(task, await agent.tasks.listAttempts(taskId));
58
+ const snapshot = buildTaskSnapshot(task, await withAgent(agentNode, (agent) => agent.tasks.listAttempts(taskId)));
48
59
  const resultMsg = RED.util.cloneMessage(msg);
49
- resultMsg.payload = snapshot;
60
+ resultMsg.payload = correlationId ? {
61
+ ...snapshot,
62
+ correlationId
63
+ } : snapshot;
64
+ if (correlationId) resultMsg.correlationId = correlationId;
65
+ active.delete(taskId);
50
66
  this.status({
51
67
  fill: snapshot.accepted ? "green" : "red",
52
68
  shape: "dot",
53
- text: `${snapshot.status}${snapshot.accepted ? " " : ""}`
69
+ text: statusText(`${snapshot.status}${snapshot.accepted ? " ok" : ""}`, label, active.size)
54
70
  });
55
71
  send([null, resultMsg]);
56
72
  done();
@@ -61,15 +77,16 @@ var init = (RED) => {
61
77
  this.status({
62
78
  fill: "blue",
63
79
  shape: "ring",
64
- text: `${task.status} · ${polls}×`
80
+ text: statusText(`${task.status} ${polls}x`, label, active.size)
65
81
  });
66
82
  await sleep(pollMs, pending);
67
83
  }
68
84
  } catch (err) {
85
+ if (taskIdForStatus) active.delete(taskIdForStatus);
69
86
  this.status({
70
87
  fill: "red",
71
88
  shape: "ring",
72
- text: "error"
89
+ text: statusText("error", label, active.size)
73
90
  });
74
91
  done(err instanceof Error ? err : new Error(String(err)));
75
92
  }
@@ -120,6 +137,43 @@ function resolveTaskId(msg, configured) {
120
137
  }
121
138
  return configured && configured.length > 0 ? configured : void 0;
122
139
  }
140
+ function resolveCorrelationId(msg) {
141
+ if (typeof msg.correlationId === "string" && msg.correlationId) return msg.correlationId;
142
+ const payload = msg.payload;
143
+ if (payload && typeof payload === "object") {
144
+ const p = payload;
145
+ if (typeof p.correlationId === "string" && p.correlationId) return p.correlationId;
146
+ }
147
+ }
148
+ function describeMessage(msg) {
149
+ if (typeof msg.reviewDimension === "string" && msg.reviewDimension) return msg.reviewDimension;
150
+ const payload = msg.payload;
151
+ if (payload && typeof payload === "object") {
152
+ const p = payload;
153
+ if (typeof p.dimension === "string" && p.dimension) return p.dimension;
154
+ if (typeof p.title === "string" && p.title) return p.title;
155
+ }
156
+ return "task";
157
+ }
158
+ function describeWait(taskId, msg, fallback) {
159
+ if (fallback !== "task") return fallback;
160
+ const payload = msg.payload;
161
+ if (payload && typeof payload === "object") {
162
+ const p = payload;
163
+ if (typeof p.title === "string" && p.title) return p.title;
164
+ }
165
+ return shortId(taskId);
166
+ }
167
+ function statusText(action, label, activeCount) {
168
+ const suffix = activeCount > 0 ? ` · ${activeCount} active` : "";
169
+ return `${action} · ${truncate(label, 34)}${suffix}`;
170
+ }
171
+ function shortId(id) {
172
+ return id.slice(0, 8);
173
+ }
174
+ function truncate(value, max) {
175
+ return value.length <= max ? value : `${value.slice(0, max - 1)}…`;
176
+ }
123
177
  function sleep(ms, pending) {
124
178
  return new Promise((resolve) => {
125
179
  const t = setTimeout(() => {
@@ -1,3 +1,4 @@
1
+ import { t as withAgent } from "./agent-call.js";
1
2
  import { randomUUID } from "node:crypto";
2
3
  //#region src/nodes/tasks-create.ts
3
4
  var init = (RED) => {
@@ -5,18 +6,24 @@ var init = (RED) => {
5
6
  RED.nodes.createNode(this, def);
6
7
  const agentNode = def.agent ? RED.nodes.getNode(def.agent) : null;
7
8
  const profileNode = def.runtimeProfile ? RED.nodes.getNode(def.runtimeProfile) : null;
9
+ const active = /* @__PURE__ */ new Map();
10
+ let nextInvocationId = 0;
8
11
  this.on("input", (msg, send, done) => {
12
+ const invocationId = ++nextInvocationId;
13
+ let label = describeMessage(msg);
9
14
  const run = async () => {
10
15
  try {
11
16
  if (!agentNode || typeof agentNode.getAgent !== "function") throw new Error("tasks-create: no moltnet-agent configured");
17
+ active.set(invocationId, label);
12
18
  this.status({
13
19
  fill: "blue",
14
20
  shape: "dot",
15
- text: "creating"
21
+ text: statusText("creating", label, active.size)
16
22
  });
17
- const agent = await agentNode.getAgent();
18
23
  const base = msg.payload && typeof msg.payload === "object" ? { ...msg.payload } : {};
19
24
  if (!base.taskType) base.taskType = "freeform";
25
+ label = describeTaskBody(base, label);
26
+ active.set(invocationId, label);
20
27
  if (!base.allowedProfiles && profileNode?.profileId) base.allowedProfiles = [{ profileId: profileNode.profileId }];
21
28
  if (base.maxAttempts === void 0 && typeof def.maxAttempts === "number") base.maxAttempts = def.maxAttempts;
22
29
  if (!base.teamId && agentNode.teamId) base.teamId = agentNode.teamId;
@@ -24,22 +31,24 @@ var init = (RED) => {
24
31
  const correlationId = resolveCorrelationId(msg, base.correlationId, def.generateCorrelationId === true);
25
32
  if (correlationId) base.correlationId = correlationId;
26
33
  const { teamId, ...createBody } = base;
27
- const task = await agent.tasks.create(createBody, { teamId });
34
+ const task = await withAgent(agentNode, (agent) => agent.tasks.create(createBody, { teamId }));
28
35
  const out = RED.util.cloneMessage(msg);
29
36
  if (correlationId) out.correlationId = correlationId;
30
37
  out.payload = task;
38
+ active.delete(invocationId);
31
39
  this.status({
32
40
  fill: "green",
33
41
  shape: "dot",
34
- text: `task ${task.id ?? "created"}`
42
+ text: statusText(`created ${shortId(task.id)}`, label, active.size)
35
43
  });
36
44
  send(out);
37
45
  done();
38
46
  } catch (err) {
47
+ active.delete(invocationId);
39
48
  this.status({
40
49
  fill: "red",
41
50
  shape: "ring",
42
- text: "error"
51
+ text: statusText("error", label, active.size)
43
52
  });
44
53
  done(err instanceof Error ? err : new Error(String(err)));
45
54
  }
@@ -60,5 +69,37 @@ function resolveCorrelationId(msg, fromPayload, generate) {
60
69
  if (typeof msg.correlationId === "string" && msg.correlationId) return msg.correlationId;
61
70
  return generate ? randomUUID() : void 0;
62
71
  }
72
+ function describeMessage(msg) {
73
+ if (typeof msg.reviewDimension === "string" && msg.reviewDimension) return msg.reviewDimension;
74
+ const payload = msg.payload;
75
+ if (payload && typeof payload === "object") {
76
+ const p = payload;
77
+ if (typeof p.dimension === "string" && p.dimension) return p.dimension;
78
+ if (typeof p.title === "string" && p.title) return p.title;
79
+ }
80
+ return "task";
81
+ }
82
+ function describeTaskBody(body, fallback) {
83
+ if (fallback !== "task") return fallback;
84
+ const input = body.input;
85
+ if (input && typeof input === "object") {
86
+ const execution = input.execution;
87
+ if (execution && typeof execution === "object") {
88
+ const dimension = execution.dimension;
89
+ if (typeof dimension === "string" && dimension) return dimension;
90
+ }
91
+ }
92
+ return typeof body.title === "string" && body.title ? body.title : fallback;
93
+ }
94
+ function statusText(action, label, activeCount) {
95
+ const suffix = activeCount > 0 ? ` · ${activeCount} active` : "";
96
+ return `${action} · ${truncate(label, 34)}${suffix}`;
97
+ }
98
+ function shortId(id) {
99
+ return typeof id === "string" && id ? id.slice(0, 8) : "task";
100
+ }
101
+ function truncate(value, max) {
102
+ return value.length <= max ? value : `${value.slice(0, max - 1)}…`;
103
+ }
63
104
  //#endregion
64
105
  export { init as default };
@@ -1,3 +1,4 @@
1
+ import { t as withAgent } from "./agent-call.js";
1
2
  import { a as nonEmpty, c as positiveInt, n as compact, r as csv, t as bool } from "./query-utils.js";
2
3
  //#region src/nodes/tasks-list.ts
3
4
  var init = (RED) => {
@@ -15,9 +16,8 @@ var init = (RED) => {
15
16
  shape: "dot",
16
17
  text: "loading…"
17
18
  });
18
- const agent = await agentNode.getAgent();
19
19
  const query = buildTasksQuery(def, msg);
20
- const result = await agent.tasks.list(query, { teamId });
20
+ const result = await withAgent(agentNode, (agent) => agent.tasks.list(query, { teamId }));
21
21
  const out = RED.util.cloneMessage(msg);
22
22
  out.payload = result.items;
23
23
  out.tasks = {
@@ -1,3 +1,4 @@
1
+ import { t as withAgent } from "./agent-call.js";
1
2
  //#region src/nodes/workflow-status.ts
2
3
  var init = (RED) => {
3
4
  function WorkflowStatusNode(def) {
@@ -17,12 +18,11 @@ var init = (RED) => {
17
18
  });
18
19
  const teamId = agentNode.teamId;
19
20
  if (!teamId) throw new Error("workflow-status: agent teamId is required");
20
- const agent = await agentNode.getAgent();
21
21
  const query = {
22
22
  correlationId,
23
23
  limit: def.limit && def.limit > 0 ? def.limit : 50
24
24
  };
25
- const res = await agent.tasks.list(query, { teamId });
25
+ const res = await withAgent(agentNode, (agent) => agent.tasks.list(query, { teamId }));
26
26
  const rows = res.items.map((t) => ({
27
27
  taskId: t.id,
28
28
  type: t.taskType,