@themoltnet/node-red-contrib-core 0.8.0 → 0.10.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/README.md CHANGED
@@ -329,6 +329,75 @@ in the Node-RED instance) to get a live workflow cockpit without any custom Vue
329
329
  widget. A starter flow is in [`examples/cockpit.flow.json`](./examples/cockpit.flow.json)
330
330
  (inject → `moltnet-workflow-status` → debug; add a `ui-table` to visualize).
331
331
 
332
+ ## Axiom alert triage workflow
333
+
334
+ [`examples/axiom-alert-triage.flow.json`](./examples/axiom-alert-triage.flow.json)
335
+ is an observability triage experiment with two trigger modes:
336
+
337
+ `Axiom monitor history polling` → **parser/enricher task** →
338
+ **codebase triage task** → **judge task**.
339
+
340
+ When Axiom custom webhook notifiers are available, the same triage chain can also
341
+ start from `POST /axiom/monitor`.
342
+
343
+ The polling path calls Axiom monitor history with `AXIOM_MONITORING_TOKEN`
344
+ (`AXIOM_API_TOKEN` is accepted as a fallback for local experiments), dedupes
345
+ events by monitor/state/timestamp in flow context, starts work only for
346
+ trigger-like events, builds task bodies through `task: build`, and chains tasks
347
+ through `task: read` `artifactRef` values so large telemetry artifacts stay in
348
+ MoltNet. Resolution/no-op events go to a debug lane instead of starting codebase
349
+ triage.
350
+
351
+ Polling environment:
352
+
353
+ - `AXIOM_MONITORING_TOKEN`: Axiom token with monitor history/read access and,
354
+ for the current single-token experiment, telemetry query read access.
355
+ - `AXIOM_MONITOR_IDS`: optional comma-separated monitor ids. Defaults to the
356
+ MoltNet monitors created by `infra/axiom/monitors/apply.mjs`.
357
+ - `AXIOM_POLL_WINDOW_MINUTES`: optional lookback window, default `15`.
358
+
359
+ To test the flow without a real Axiom trigger, use the **inject synthetic
360
+ alert** node in the example. It builds the same normalized `msg.axiom` shape as
361
+ the polling/webhook paths, marks it with `synthetic: true`, and routes it through
362
+ the common monitor-events link so the parser, triage, artifact chaining, and
363
+ judge stages all run.
364
+
365
+ Optional Axiom custom webhook setup:
366
+
367
+ - URL: `https://<node-red-host>/axiom/monitor`
368
+ - Method: `POST`
369
+ - Optional secret header: `x-axiom-webhook-secret: <secret>`
370
+ - Node-RED env: `AXIOM_WEBHOOK_SECRET=<secret>` when that header is used
371
+
372
+ Suggested custom webhook body:
373
+
374
+ ```json
375
+ {
376
+ "action": "{{.Action}}",
377
+ "body": "{{.Body}}",
378
+ "endTime": "now",
379
+ "monitorId": "{{.MonitorID}}",
380
+ "monitorName": "{{.Title}}",
381
+ "service": "moltnet-rest-api",
382
+ "severity": "warning",
383
+ "startTime": "now-30m",
384
+ "value": "{{.Value}}"
385
+ }
386
+ ```
387
+
388
+ The parser/enricher task prompt asks the agent to use the repo-local
389
+ `axiom-alert-triage` skill and produce a JSON artifact titled
390
+ `telemetry-evidence`. The next task consumes that artifact, compares findings
391
+ against the `getlarge/themoltnet` codebase, GitHub issues, and MoltNet entries,
392
+ then writes `triage-report`. The final judge task writes `triage-judgment`.
393
+
394
+ Before enabling live webhooks, refine Axiom monitors so broad auth/client noise
395
+ (401/404) is separated from actionable alerts such as 5xx errors, latency
396
+ regression, event loop pressure, and memory pressure. The companion repo-local
397
+ `axiom-observability` skill and [`infra/axiom`](../../infra/axiom) document the
398
+ monitor/dashboard maintenance conventions and provide idempotent apply scripts
399
+ for the committed Axiom config.
400
+
332
401
  ## Build
333
402
 
334
403
  ```bash
@@ -389,7 +458,9 @@ export default {
389
458
  Open the editor, drag in `agent` + the task nodes, or import
390
459
  [`examples/issue-lifecycle.flow.json`](./examples/issue-lifecycle.flow.json) or
391
460
  [`examples/cockpit.flow.json`](./examples/cockpit.flow.json) or
392
- [`examples/deep-review-freeform.flow.json`](./examples/deep-review-freeform.flow.json),
461
+ [`examples/deep-review-freeform.flow.json`](./examples/deep-review-freeform.flow.json)
462
+ or
463
+ [`examples/axiom-alert-triage.flow.json`](./examples/axiom-alert-triage.flow.json),
393
464
  then fill the agent's `clientId`/`clientSecret`.
394
465
 
395
466
  If Node-RED crashes in `@node-red/editor-api/lib/auth/tokens.js` with
@@ -0,0 +1,74 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('moltnet-runtime-session-download', {
3
+ category: 'moltnet',
4
+ color: '#00d4c8',
5
+ paletteLabel: 'runtime session: download',
6
+ defaults: {
7
+ name: { value: '' },
8
+ agent: { value: '', type: 'moltnet-agent', required: true },
9
+ taskId: { value: '' },
10
+ teamId: { value: '' },
11
+ allowMsgTeamOverride: { value: false },
12
+ attemptN: { value: '', validate: RED.validators.number() },
13
+ maxBytes: { value: 26214400, validate: RED.validators.number() },
14
+ },
15
+ inputs: 1,
16
+ outputs: 1,
17
+ icon: 'font-awesome/fa-download',
18
+ label: function () {
19
+ return this.name || 'runtime session: download';
20
+ },
21
+ });
22
+ </script>
23
+
24
+ <script type="text/html" data-template-name="moltnet-runtime-session-download">
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-attemptN"
58
+ ><i class="fa fa-history"></i> Attempt</label
59
+ >
60
+ <input type="number" id="node-input-attemptN" placeholder="msg.attemptN" />
61
+ </div>
62
+ <div class="form-row">
63
+ <label for="node-input-maxBytes"><i class="fa fa-database"></i> Max</label>
64
+ <input type="number" id="node-input-maxBytes" />
65
+ </div>
66
+ </script>
67
+
68
+ <script type="text/html" data-help-name="moltnet-runtime-session-download">
69
+ <p>
70
+ Downloads durable runtime session content for a task attempt. Emits session
71
+ bytes as a Buffer on <code>msg.payload</code> and context metadata on
72
+ <code>msg.runtimeSession</code>. The local byte limit defaults to 25 MiB.
73
+ </p>
74
+ </script>
@@ -0,0 +1,57 @@
1
+ import { t as withAgent } from "./agent-call.js";
2
+ import { t as bool } from "./query-utils.js";
3
+ import { a as requireAttemptContext, s as resolveMaxBytes, t as collectArtifactBody } from "./task-artifact-utils.js";
4
+ //#region src/nodes/runtime-session-download.ts
5
+ var init = (RED) => {
6
+ function RuntimeSessionDownloadNode(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("runtime-session-download: no moltnet-agent configured");
13
+ const { taskId, teamId, attemptN } = requireAttemptContext("runtime-session-download", msg, def.taskId, def.teamId, def.attemptN, agentNode, bool(def.allowMsgTeamOverride) ?? false);
14
+ this.status({
15
+ fill: "blue",
16
+ shape: "dot",
17
+ text: "downloading…"
18
+ });
19
+ const body = await collectArtifactBody(await withAgent(agentNode, (agent) => agent.runtimeSessions.download({
20
+ taskId,
21
+ attemptN
22
+ }, { teamId })), resolveMaxBytes(def.maxBytes), "runtime-session-download");
23
+ const out = RED.util.cloneMessage({
24
+ ...msg,
25
+ payload: void 0
26
+ });
27
+ out.payload = body;
28
+ out.taskId = taskId;
29
+ out.runtimeSession = {
30
+ taskId,
31
+ teamId,
32
+ attemptN,
33
+ sizeBytes: body.byteLength
34
+ };
35
+ this.status({
36
+ fill: "green",
37
+ shape: "dot",
38
+ text: `${body.byteLength} byte(s)`
39
+ });
40
+ send(out);
41
+ done();
42
+ } catch (err) {
43
+ this.status({
44
+ fill: "red",
45
+ shape: "ring",
46
+ text: "error"
47
+ });
48
+ done(err instanceof Error ? err : new Error(String(err)));
49
+ }
50
+ };
51
+ run();
52
+ });
53
+ }
54
+ RED.nodes.registerType("moltnet-runtime-session-download", RuntimeSessionDownloadNode);
55
+ };
56
+ //#endregion
57
+ export { init as default };
@@ -0,0 +1,73 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('moltnet-runtime-session-get', {
3
+ category: 'moltnet',
4
+ color: '#00d4c8',
5
+ paletteLabel: 'runtime session: get',
6
+ defaults: {
7
+ name: { value: '' },
8
+ agent: { value: '', type: 'moltnet-agent', required: true },
9
+ taskId: { value: '' },
10
+ teamId: { value: '' },
11
+ allowMsgTeamOverride: { value: false },
12
+ attemptN: { value: '', validate: RED.validators.number() },
13
+ },
14
+ inputs: 1,
15
+ outputs: 1,
16
+ icon: 'font-awesome/fa-info-circle',
17
+ label: function () {
18
+ return this.name || 'runtime session: get';
19
+ },
20
+ });
21
+ </script>
22
+
23
+ <script type="text/html" data-template-name="moltnet-runtime-session-get">
24
+ <div class="form-row">
25
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
26
+ <input type="text" id="node-input-name" />
27
+ </div>
28
+ <div class="form-row">
29
+ <label for="node-input-agent"><i class="fa fa-user"></i> Agent</label>
30
+ <input type="text" id="node-input-agent" />
31
+ </div>
32
+ <div class="form-row">
33
+ <label for="node-input-taskId"><i class="fa fa-hashtag"></i> Task ID</label>
34
+ <input
35
+ type="text"
36
+ id="node-input-taskId"
37
+ placeholder="msg.payload.taskId"
38
+ />
39
+ </div>
40
+ <div class="form-row">
41
+ <label for="node-input-teamId"><i class="fa fa-users"></i> Team ID</label>
42
+ <input type="text" id="node-input-teamId" placeholder="agent teamId" />
43
+ </div>
44
+ <div class="form-row">
45
+ <label for="node-input-allowMsgTeamOverride"
46
+ ><i class="fa fa-random"></i> Team override</label
47
+ >
48
+ <input
49
+ type="checkbox"
50
+ id="node-input-allowMsgTeamOverride"
51
+ style="display: inline-block; width: auto; vertical-align: top"
52
+ />
53
+ <span>Allow <code>msg.teamId</code></span>
54
+ </div>
55
+ <div class="form-row">
56
+ <label for="node-input-attemptN"
57
+ ><i class="fa fa-history"></i> Attempt</label
58
+ >
59
+ <input type="number" id="node-input-attemptN" placeholder="msg.attemptN" />
60
+ </div>
61
+ </script>
62
+
63
+ <script type="text/html" data-help-name="moltnet-runtime-session-get">
64
+ <p>
65
+ Fetches durable runtime session metadata for a task attempt. Team ID is
66
+ taken from this node or the configured agent unless Team override is
67
+ enabled.
68
+ </p>
69
+ <p>
70
+ Emits the metadata or <code>null</code> on <code>msg.payload</code> and a
71
+ context envelope on <code>msg.runtimeSession</code>.
72
+ </p>
73
+ </script>
@@ -0,0 +1,54 @@
1
+ import { t as withAgent } from "./agent-call.js";
2
+ import { t as bool } from "./query-utils.js";
3
+ import { a as requireAttemptContext } from "./task-artifact-utils.js";
4
+ //#region src/nodes/runtime-session-get.ts
5
+ var init = (RED) => {
6
+ function RuntimeSessionGetNode(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("runtime-session-get: no moltnet-agent configured");
13
+ const { taskId, teamId, attemptN } = requireAttemptContext("runtime-session-get", msg, def.taskId, def.teamId, def.attemptN, agentNode, bool(def.allowMsgTeamOverride) ?? false);
14
+ this.status({
15
+ fill: "blue",
16
+ shape: "dot",
17
+ text: "loading…"
18
+ });
19
+ const session = await withAgent(agentNode, (agent) => agent.runtimeSessions.getForAttempt({
20
+ taskId,
21
+ attemptN
22
+ }, { teamId }));
23
+ const out = RED.util.cloneMessage(msg);
24
+ out.payload = session;
25
+ out.taskId = taskId;
26
+ out.runtimeSession = {
27
+ taskId,
28
+ teamId,
29
+ attemptN,
30
+ session
31
+ };
32
+ this.status({
33
+ fill: session ? "green" : "yellow",
34
+ shape: session ? "dot" : "ring",
35
+ text: session ? "found" : "not found"
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-runtime-session-get", RuntimeSessionGetNode);
52
+ };
53
+ //#endregion
54
+ export { init as default };
@@ -0,0 +1,120 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('moltnet-runtime-session-upload', {
3
+ category: 'moltnet',
4
+ color: '#00d4c8',
5
+ paletteLabel: 'runtime session: upload',
6
+ defaults: {
7
+ name: { value: '' },
8
+ agent: { value: '', type: 'moltnet-agent', required: true },
9
+ taskId: { value: '' },
10
+ teamId: { value: '' },
11
+ allowMsgTeamOverride: { value: false },
12
+ attemptN: { value: '', validate: RED.validators.number() },
13
+ maxBytes: { value: 26214400, validate: RED.validators.number() },
14
+ sessionKind: { value: 'root' },
15
+ parentSessionId: { value: '' },
16
+ sourceSlotId: { value: '' },
17
+ sourceRuntimeProfileId: { value: '' },
18
+ },
19
+ inputs: 1,
20
+ outputs: 1,
21
+ icon: 'font-awesome/fa-upload',
22
+ label: function () {
23
+ return this.name || 'runtime session: upload';
24
+ },
25
+ });
26
+ </script>
27
+
28
+ <script type="text/html" data-template-name="moltnet-runtime-session-upload">
29
+ <div class="form-row">
30
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
31
+ <input type="text" id="node-input-name" />
32
+ </div>
33
+ <div class="form-row">
34
+ <label for="node-input-agent"><i class="fa fa-user"></i> Agent</label>
35
+ <input type="text" id="node-input-agent" />
36
+ </div>
37
+ <div class="form-row">
38
+ <label for="node-input-taskId"><i class="fa fa-hashtag"></i> Task ID</label>
39
+ <input
40
+ type="text"
41
+ id="node-input-taskId"
42
+ placeholder="msg.payload.taskId"
43
+ />
44
+ </div>
45
+ <div class="form-row">
46
+ <label for="node-input-teamId"><i class="fa fa-users"></i> Team ID</label>
47
+ <input type="text" id="node-input-teamId" placeholder="agent teamId" />
48
+ </div>
49
+ <div class="form-row">
50
+ <label for="node-input-allowMsgTeamOverride"
51
+ ><i class="fa fa-random"></i> Team override</label
52
+ >
53
+ <input
54
+ type="checkbox"
55
+ id="node-input-allowMsgTeamOverride"
56
+ style="display: inline-block; width: auto; vertical-align: top"
57
+ />
58
+ <span>Allow <code>msg.teamId</code></span>
59
+ </div>
60
+ <div class="form-row">
61
+ <label for="node-input-attemptN"
62
+ ><i class="fa fa-history"></i> Attempt</label
63
+ >
64
+ <input type="number" id="node-input-attemptN" placeholder="msg.attemptN" />
65
+ </div>
66
+ <div class="form-row">
67
+ <label for="node-input-sessionKind"
68
+ ><i class="fa fa-code-fork"></i> Kind</label
69
+ >
70
+ <select id="node-input-sessionKind">
71
+ <option value="root">Root</option>
72
+ <option value="extend">Extend</option>
73
+ <option value="fork">Fork</option>
74
+ </select>
75
+ </div>
76
+ <div class="form-row">
77
+ <label for="node-input-parentSessionId"
78
+ ><i class="fa fa-link"></i> Parent</label
79
+ >
80
+ <input
81
+ type="text"
82
+ id="node-input-parentSessionId"
83
+ placeholder="parent runtime session UUID"
84
+ />
85
+ </div>
86
+ <div class="form-row">
87
+ <label for="node-input-sourceSlotId"
88
+ ><i class="fa fa-map-pin"></i> Slot</label
89
+ >
90
+ <input type="text" id="node-input-sourceSlotId" placeholder="slot UUID" />
91
+ </div>
92
+ <div class="form-row">
93
+ <label for="node-input-sourceRuntimeProfileId"
94
+ ><i class="fa fa-cog"></i> Profile</label
95
+ >
96
+ <input
97
+ type="text"
98
+ id="node-input-sourceRuntimeProfileId"
99
+ placeholder="runtime profile UUID"
100
+ />
101
+ </div>
102
+ <div class="form-row">
103
+ <label for="node-input-maxBytes"><i class="fa fa-database"></i> Max</label>
104
+ <input type="number" id="node-input-maxBytes" />
105
+ </div>
106
+ </script>
107
+
108
+ <script type="text/html" data-help-name="moltnet-runtime-session-upload">
109
+ <p>
110
+ Uploads durable runtime session bytes for a task attempt. The body is read
111
+ from <code>msg.payload</code> when it is a string, Buffer, Uint8Array, or
112
+ ArrayBuffer. Object payloads may provide <code>content</code>,
113
+ <code>body</code>, or base64 <code>contentBase64</code>.
114
+ </p>
115
+ <p>
116
+ Session kind and optional parent/source ids can be supplied by node fields
117
+ or matching fields on <code>msg.payload</code>. Emits session metadata on
118
+ <code>msg.payload</code> and <code>msg.runtimeSession</code>.
119
+ </p>
120
+ </script>
@@ -0,0 +1,62 @@
1
+ import { t as withAgent } from "./agent-call.js";
2
+ import { n as compact, t as bool } from "./query-utils.js";
3
+ import { a as requireAttemptContext, c as resolveUploadBody, o as resolveField, s as resolveMaxBytes } from "./task-artifact-utils.js";
4
+ //#region src/nodes/runtime-session-upload.ts
5
+ var init = (RED) => {
6
+ function RuntimeSessionUploadNode(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("runtime-session-upload: no moltnet-agent configured");
13
+ const { taskId, teamId, attemptN } = requireAttemptContext("runtime-session-upload", msg, def.taskId, def.teamId, def.attemptN, agentNode, bool(def.allowMsgTeamOverride) ?? false);
14
+ const body = resolveUploadBody(msg, resolveMaxBytes(def.maxBytes), "runtime-session-upload");
15
+ const query = buildUploadQuery(def, msg);
16
+ this.status({
17
+ fill: "blue",
18
+ shape: "dot",
19
+ text: "uploading…"
20
+ });
21
+ const session = await withAgent(agentNode, (agent) => agent.runtimeSessions.upload({
22
+ taskId,
23
+ attemptN
24
+ }, body, query, { teamId }));
25
+ const out = RED.util.cloneMessage({
26
+ ...msg,
27
+ payload: void 0
28
+ });
29
+ out.payload = session;
30
+ out.taskId = taskId;
31
+ out.runtimeSession = session;
32
+ this.status({
33
+ fill: "green",
34
+ shape: "dot",
35
+ text: session.sha256 ?? "uploaded"
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-runtime-session-upload", RuntimeSessionUploadNode);
52
+ };
53
+ function buildUploadQuery(def, msg) {
54
+ return compact({
55
+ sessionKind: resolveField(msg, "sessionKind", def.sessionKind) ?? resolveField(msg, "session_kind", void 0) ?? "root",
56
+ parentSessionId: resolveField(msg, "parentSessionId", def.parentSessionId) ?? resolveField(msg, "parent_session_id", void 0),
57
+ sourceSlotId: resolveField(msg, "sourceSlotId", def.sourceSlotId) ?? resolveField(msg, "source_slot_id", void 0),
58
+ sourceRuntimeProfileId: resolveField(msg, "sourceRuntimeProfileId", def.sourceRuntimeProfileId) ?? resolveField(msg, "source_runtime_profile_id", void 0)
59
+ });
60
+ }
61
+ //#endregion
62
+ export { init as default };
package/dist/nodes/src.js CHANGED
@@ -1927,7 +1927,7 @@ var findLatestRuntimeSlotForAttempt = (options) => (options.client ?? client).ge
1927
1927
  ...options
1928
1928
  });
1929
1929
  /**
1930
- * Delete terminal tasks in bulk. Safe mode skips live, unauthorized, missing, and protected tasks.
1930
+ * Queue asynchronous deletion of terminal tasks in bulk. By default, live, unauthorized, missing, and protected tasks are skipped. Set force: true with a reason to delete protected terminal tasks.
1931
1931
  */
1932
1932
  var batchDeleteTasks = (options) => (options.client ?? client).delete({
1933
1933
  security: [
@@ -45,75 +45,75 @@ function resolveField(msg, name, configured) {
45
45
  function resolveMaxBytes(configured) {
46
46
  return positiveInt(configured) ?? 26214400;
47
47
  }
48
- function resolveUploadBody(msg, maxBytes) {
48
+ function resolveUploadBody(msg, maxBytes, nodeName = "task-artifact-upload") {
49
49
  const payload = msg.payload;
50
- if (Buffer.isBuffer(payload)) return enforceMaxBytes(payload, maxBytes);
51
- if (payload instanceof Uint8Array) return enforceMaxBytes(payload, maxBytes);
50
+ if (Buffer.isBuffer(payload)) return enforceMaxBytes(payload, maxBytes, nodeName);
51
+ if (payload instanceof Uint8Array) return enforceMaxBytes(payload, maxBytes, nodeName);
52
52
  if (payload instanceof ArrayBuffer) {
53
- if (payload.byteLength > maxBytes) throw tooLarge("upload", maxBytes);
53
+ if (payload.byteLength > maxBytes) throw tooLarge("upload", maxBytes, nodeName);
54
54
  return new Uint8Array(payload);
55
55
  }
56
56
  if (typeof payload === "string") {
57
- if (Buffer.byteLength(payload) > maxBytes) throw tooLarge("upload", maxBytes);
57
+ if (Buffer.byteLength(payload) > maxBytes) throw tooLarge("upload", maxBytes, nodeName);
58
58
  return new TextEncoder().encode(payload);
59
59
  }
60
60
  const record = payloadRecord(msg);
61
61
  if (typeof record.contentBase64 === "string") {
62
62
  const normalized = record.contentBase64.replace(/\s/g, "");
63
- if (decodedBase64Length(normalized) > maxBytes) throw tooLarge("upload", maxBytes);
63
+ if (decodedBase64Length(normalized) > maxBytes) throw tooLarge("upload", maxBytes, nodeName);
64
64
  return Buffer.from(normalized, "base64");
65
65
  }
66
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);
67
+ if (Buffer.isBuffer(content)) return enforceMaxBytes(content, maxBytes, nodeName);
68
+ if (content instanceof Uint8Array) return enforceMaxBytes(content, maxBytes, nodeName);
69
69
  if (content instanceof ArrayBuffer) {
70
- if (content.byteLength > maxBytes) throw tooLarge("upload", maxBytes);
70
+ if (content.byteLength > maxBytes) throw tooLarge("upload", maxBytes, nodeName);
71
71
  return new Uint8Array(content);
72
72
  }
73
73
  if (typeof content === "string") {
74
- if (Buffer.byteLength(content) > maxBytes) throw tooLarge("upload", maxBytes);
74
+ if (Buffer.byteLength(content) > maxBytes) throw tooLarge("upload", maxBytes, nodeName);
75
75
  return new TextEncoder().encode(content);
76
76
  }
77
- throw new Error("task-artifact-upload: payload content is required");
77
+ throw new Error(`${nodeName}: payload content is required`);
78
78
  }
79
- async function collectArtifactBody(value, maxBytes) {
79
+ async function collectArtifactBody(value, maxBytes, nodeName = "task-artifact-download") {
80
80
  const source = value && typeof value === "object" && "stream" in value ? value.stream : value;
81
81
  if (Buffer.isBuffer(source)) {
82
- if (source.byteLength > maxBytes) throw tooLarge("download", maxBytes);
82
+ if (source.byteLength > maxBytes) throw tooLarge("download", maxBytes, nodeName);
83
83
  return source;
84
84
  }
85
85
  if (source instanceof Uint8Array) {
86
- if (source.byteLength > maxBytes) throw tooLarge("download", maxBytes);
86
+ if (source.byteLength > maxBytes) throw tooLarge("download", maxBytes, nodeName);
87
87
  return Buffer.from(source);
88
88
  }
89
89
  if (source instanceof ArrayBuffer) {
90
- if (source.byteLength > maxBytes) throw tooLarge("download", maxBytes);
90
+ if (source.byteLength > maxBytes) throw tooLarge("download", maxBytes, nodeName);
91
91
  return Buffer.from(source);
92
92
  }
93
93
  if (typeof source === "string") {
94
- if (Buffer.byteLength(source) > maxBytes) throw tooLarge("download", maxBytes);
94
+ if (Buffer.byteLength(source) > maxBytes) throw tooLarge("download", maxBytes, nodeName);
95
95
  return Buffer.from(source);
96
96
  }
97
97
  if (source instanceof Readable) {
98
98
  const chunks = [];
99
99
  let bytes = 0;
100
- for await (const chunk of source) bytes = pushChunk(chunks, chunk, bytes, maxBytes);
100
+ for await (const chunk of source) bytes = pushChunk(chunks, chunk, bytes, maxBytes, nodeName);
101
101
  return Buffer.concat(chunks);
102
102
  }
103
103
  if (source && typeof source === "object" && Symbol.asyncIterator in source) {
104
104
  const chunks = [];
105
105
  let bytes = 0;
106
- for await (const chunk of source) bytes = pushChunk(chunks, chunk, bytes, maxBytes);
106
+ for await (const chunk of source) bytes = pushChunk(chunks, chunk, bytes, maxBytes, nodeName);
107
107
  return Buffer.concat(chunks);
108
108
  }
109
109
  if (source && typeof source === "object" && "arrayBuffer" in source) {
110
110
  const size = source.size;
111
- if (typeof size === "number" && size > maxBytes) throw tooLarge("download", maxBytes);
111
+ if (typeof size === "number" && size > maxBytes) throw tooLarge("download", maxBytes, nodeName);
112
112
  const arrayBuffer = await source.arrayBuffer();
113
- if (arrayBuffer.byteLength > maxBytes) throw tooLarge("download", maxBytes);
113
+ if (arrayBuffer.byteLength > maxBytes) throw tooLarge("download", maxBytes, nodeName);
114
114
  return Buffer.from(arrayBuffer);
115
115
  }
116
- throw new Error("task-artifact-download: unsupported artifact body");
116
+ throw new Error(`${nodeName}: unsupported downloaded body`);
117
117
  }
118
118
  function toBuffer(value) {
119
119
  if (Buffer.isBuffer(value)) return value;
@@ -126,14 +126,14 @@ function recordField(value, key) {
126
126
  if (!value || typeof value !== "object") return void 0;
127
127
  return value[key];
128
128
  }
129
- function enforceMaxBytes(value, maxBytes) {
130
- if (value.byteLength > maxBytes) throw tooLarge("upload", maxBytes);
129
+ function enforceMaxBytes(value, maxBytes, nodeName) {
130
+ if (value.byteLength > maxBytes) throw tooLarge("upload", maxBytes, nodeName);
131
131
  return value;
132
132
  }
133
- function pushChunk(chunks, chunk, bytes, maxBytes) {
133
+ function pushChunk(chunks, chunk, bytes, maxBytes, nodeName) {
134
134
  const next = toBuffer(chunk);
135
135
  const total = bytes + next.byteLength;
136
- if (total > maxBytes) throw tooLarge("download", maxBytes);
136
+ if (total > maxBytes) throw tooLarge("download", maxBytes, nodeName);
137
137
  chunks.push(next);
138
138
  return total;
139
139
  }
@@ -142,8 +142,8 @@ function decodedBase64Length(value) {
142
142
  const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
143
143
  return Math.floor(value.length * 3 / 4) - padding;
144
144
  }
145
- function tooLarge(operation, maxBytes) {
146
- return /* @__PURE__ */ new Error(`task-artifact-${operation}: artifact body exceeds ${maxBytes} bytes`);
145
+ function tooLarge(operation, maxBytes, nodeName) {
146
+ return /* @__PURE__ */ new Error(`${nodeName}: ${operation} body exceeds ${maxBytes} bytes`);
147
147
  }
148
148
  //#endregion
149
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 };