@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 +72 -1
- package/dist/nodes/runtime-session-download.html +74 -0
- package/dist/nodes/runtime-session-download.js +57 -0
- package/dist/nodes/runtime-session-get.html +73 -0
- package/dist/nodes/runtime-session-get.js +54 -0
- package/dist/nodes/runtime-session-upload.html +120 -0
- package/dist/nodes/runtime-session-upload.js +62 -0
- package/dist/nodes/src.js +1 -1
- package/dist/nodes/task-artifact-utils.js +27 -27
- package/examples/axiom-alert-triage.flow.json +663 -0
- package/package.json +5 -2
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
|
-
*
|
|
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(
|
|
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(
|
|
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(
|
|
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 };
|