@fieldwangai/agentflow 0.1.156 → 0.1.157
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/bin/lib/flow-dsl/codegen.mjs +32 -21
- package/bin/lib/flow-dsl/parser.mjs +24 -4
- package/bin/lib/jenkins.mjs +368 -0
- package/bin/lib/legacy-flow-execution.mjs +0 -1
- package/bin/lib/ui-server.mjs +72 -0
- package/bin/lib/workspace-routes.mjs +113 -9
- package/bin/lib/workspace-server.mjs +407 -5
- package/builtin/nodes/tool_jenkins_build.md +5 -4
- package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-ClNxY2Wu.js → WorkflowAssistantThread-g_ljSViJ.js} +1 -1
- package/builtin/web-ui/dist/assets/index-C0iq6zHl.js +870 -0
- package/builtin/web-ui/dist/assets/index-mmXs3H9P.css +1 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/skills/agentflow-flow-dsl/SKILL.md +13 -0
- package/skills/agentflow-flow-dsl/references/node-calls.md +1 -0
- package/skills/agentflow-node-dsl/SKILL.md +27 -3
- package/skills/agentflow-node-reference/references/builtin-nodes.md +8 -0
- package/builtin/web-ui/dist/assets/index-BJzMYRK3.js +0 -870
- package/builtin/web-ui/dist/assets/index-CEXmmwM2.css +0 -1
|
@@ -18,6 +18,7 @@ import { buildGitContext, inferGitRepoRootFromWorktree, loadGitWorktree, normali
|
|
|
18
18
|
import { createGitLabMergeRequest } from "./gitlab-mr.mjs";
|
|
19
19
|
import { json } from "./http-util.mjs";
|
|
20
20
|
import { t } from "./i18n.mjs";
|
|
21
|
+
import { advanceJenkinsBuild, createJenkinsHttpInvoker, jenkinsBuildStatePath, normalizeJenkinsBuildConfig, readJenkinsBuildState, writeJenkinsBuildState } from "./jenkins.mjs";
|
|
21
22
|
import { log } from "./log.mjs";
|
|
22
23
|
import { resolveMarketplaceNodePackage } from "./marketplace.mjs";
|
|
23
24
|
import { PACKAGE_ROOT, getAgentflowDataRoot, getAgentflowUserDataRoot, listAgentflowUserIds } from "./paths.mjs";
|
|
@@ -1127,9 +1128,13 @@ function normalizeWorkspaceGraphPayload(payload) {
|
|
|
1127
1128
|
|
|
1128
1129
|
export function workspaceRunTouchedNodeIds(result) {
|
|
1129
1130
|
const ids = new Set();
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1131
|
+
// A deferred run has only executed the prefix ending at the waiting node. Merging the
|
|
1132
|
+
// complete plan here would overwrite unrelated edits made while Jenkins is running.
|
|
1133
|
+
if (!result?.deferred) {
|
|
1134
|
+
for (const id of Array.isArray(result?.order) ? result.order : []) {
|
|
1135
|
+
const text = String(id || "").trim();
|
|
1136
|
+
if (text) ids.add(text);
|
|
1137
|
+
}
|
|
1133
1138
|
}
|
|
1134
1139
|
for (const event of Array.isArray(result?.events) ? result.events : []) {
|
|
1135
1140
|
const nodeId = String(event?.nodeId || "").trim();
|
|
@@ -4825,6 +4830,7 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
|
|
|
4825
4830
|
const runTmpRoot = workspaceCreateRunTmpRoot(scopedRoot, runNodeId);
|
|
4826
4831
|
const controlBranches = new Map();
|
|
4827
4832
|
const skippedNodes = new Set();
|
|
4833
|
+
let deferred = null;
|
|
4828
4834
|
const incomingControlEdgesByTarget = new Map();
|
|
4829
4835
|
for (const edge of Array.isArray(graph?.edges) ? graph.edges : []) {
|
|
4830
4836
|
const target = String(edge?.target || "");
|
|
@@ -5072,6 +5078,77 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
|
|
|
5072
5078
|
continue;
|
|
5073
5079
|
}
|
|
5074
5080
|
|
|
5081
|
+
if (defId === "tool_jenkins_build") {
|
|
5082
|
+
const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
|
|
5083
|
+
const config = normalizeJenkinsBuildConfig({
|
|
5084
|
+
job: inputValues.job || workspaceSlotValue(workspaceSlotByName(instance, "job")),
|
|
5085
|
+
parameters: inputValues.parameters || workspaceSlotValue(workspaceSlotByName(instance, "parameters")),
|
|
5086
|
+
credentialRef: inputValues.credentialRef || workspaceSlotValue(workspaceSlotByName(instance, "credentialRef")),
|
|
5087
|
+
pollInterval: inputValues.pollInterval || workspaceSlotValue(workspaceSlotByName(instance, "pollInterval")),
|
|
5088
|
+
timeout: inputValues.timeout || workspaceSlotValue(workspaceSlotByName(instance, "timeout")),
|
|
5089
|
+
});
|
|
5090
|
+
const statePath = jenkinsBuildStatePath(scopedRoot, nodeId);
|
|
5091
|
+
let state = readJenkinsBuildState(statePath);
|
|
5092
|
+
// A completed checkpoint belongs to an earlier execution. An unfinished one is always
|
|
5093
|
+
// resumed, even when the server restarted and the browser created a new run id.
|
|
5094
|
+
if (state?.phase === "complete") state = null;
|
|
5095
|
+
const invoke = createJenkinsHttpInvoker({
|
|
5096
|
+
credentialRef: config.credentialRef,
|
|
5097
|
+
env: runtimeEnv(),
|
|
5098
|
+
fetchImpl: opts.jenkinsFetch || globalThis.fetch,
|
|
5099
|
+
signal,
|
|
5100
|
+
});
|
|
5101
|
+
throwIfAborted();
|
|
5102
|
+
const result = await advanceJenkinsBuild({
|
|
5103
|
+
state,
|
|
5104
|
+
config,
|
|
5105
|
+
invoke,
|
|
5106
|
+
persistState: (checkpoint) => writeJenkinsBuildState(statePath, checkpoint),
|
|
5107
|
+
cancelled: signal?.aborted === true,
|
|
5108
|
+
runId: opts.runId || payload.runId || payload.runSessionId || "",
|
|
5109
|
+
});
|
|
5110
|
+
throwIfAborted();
|
|
5111
|
+
state = result.state;
|
|
5112
|
+
writeJenkinsBuildState(statePath, state);
|
|
5113
|
+
let nextInstance = workspaceSetOutputSlot(graph.instances[nodeId], "status", result.outputs?.status || state.status || "");
|
|
5114
|
+
nextInstance = workspaceSetOutputSlot(nextInstance, "url", result.outputs?.url || state.url || state.buildUrl || "");
|
|
5115
|
+
nextInstance = workspaceSetOutputSlot(nextInstance, "qrUrl", result.outputs?.qrUrl || state.qrUrl || "");
|
|
5116
|
+
graph.instances[nodeId] = nextInstance;
|
|
5117
|
+
emit({
|
|
5118
|
+
type: "status",
|
|
5119
|
+
nodeId,
|
|
5120
|
+
line: result.message || state.message || "Jenkins Build",
|
|
5121
|
+
phase: state.phase || "",
|
|
5122
|
+
jenkinsStatus: state.status || "",
|
|
5123
|
+
buildNumber: state.buildNumber || "",
|
|
5124
|
+
url: state.url || state.buildUrl || "",
|
|
5125
|
+
qrUrl: state.qrUrl || "",
|
|
5126
|
+
wakeAt: state.wakeAt || "",
|
|
5127
|
+
});
|
|
5128
|
+
emit({ type: "graph", nodeId, graph });
|
|
5129
|
+
if (result.kind === "waiting") {
|
|
5130
|
+
deferred = {
|
|
5131
|
+
kind: "jenkins",
|
|
5132
|
+
nodeId,
|
|
5133
|
+
phase: String(state.phase || ""),
|
|
5134
|
+
status: String(state.status || ""),
|
|
5135
|
+
message: String(result.message || state.message || "Jenkins Build"),
|
|
5136
|
+
buildNumber: String(state.buildNumber || ""),
|
|
5137
|
+
url: String(state.url || state.buildUrl || ""),
|
|
5138
|
+
qrUrl: String(state.qrUrl || ""),
|
|
5139
|
+
wakeAt: String(result.wakeAt || state.wakeAt || new Date(Date.now() + config.pollIntervalMs).toISOString()),
|
|
5140
|
+
};
|
|
5141
|
+
emit({ type: "node-waiting", nodeId, definitionId: defId, ...deferred });
|
|
5142
|
+
break;
|
|
5143
|
+
}
|
|
5144
|
+
if (result.kind === "failed") throw new Error(result.message || "Jenkins build node failed");
|
|
5145
|
+
const finalStatus = result.outputs?.status || state.status || "ERROR";
|
|
5146
|
+
const updatedDisplays = publishNodeOutput(nodeId, finalStatus);
|
|
5147
|
+
emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
|
|
5148
|
+
emit({ type: "node-done", nodeId, definitionId: defId, jenkinsStatus: finalStatus });
|
|
5149
|
+
continue;
|
|
5150
|
+
}
|
|
5151
|
+
|
|
5075
5152
|
if (defId === "tool_git_checkout") {
|
|
5076
5153
|
const repoUrl = workspaceSlotValue(workspaceSlotByName(instance, "repoUrl")).trim();
|
|
5077
5154
|
if (!repoUrl) throw new Error("Git Checkout requires repoUrl");
|
|
@@ -5569,11 +5646,11 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
|
|
|
5569
5646
|
workspaceCleanupAutoWorktrees(autoCleanupWorktrees, graph, emit);
|
|
5570
5647
|
workspaceCleanupTmpRoot(runTmpRoot, userCtx, emit);
|
|
5571
5648
|
}
|
|
5572
|
-
if (pauseNodeIds.length > 0) {
|
|
5649
|
+
if (!deferred && pauseNodeIds.length > 0) {
|
|
5573
5650
|
emit({ type: "paused", nodeIds: pauseNodeIds, message: `Workspace run paused at ${pauseNodeIds.join(", ")}` });
|
|
5574
5651
|
}
|
|
5575
5652
|
graph.updatedAt = new Date().toISOString();
|
|
5576
|
-
return { graph, events, order, pauseNodeIds };
|
|
5653
|
+
return { graph, events, order, pauseNodeIds, deferred };
|
|
5577
5654
|
}
|
|
5578
5655
|
|
|
5579
5656
|
export function isWorkspaceRunAbortError(err) {
|
|
@@ -5625,10 +5702,34 @@ export const workspaceCollaborationSubscribers = new Map();
|
|
|
5625
5702
|
|
|
5626
5703
|
export const workspaceCollaborationSequences = new Map();
|
|
5627
5704
|
|
|
5705
|
+
function emitWorkspaceCollaborationEvent(userCtx, flowSource, flowId, archived, event = {}) {
|
|
5706
|
+
const key = workspaceCollaborationEventKey(userCtx, flowSource, flowId, archived);
|
|
5707
|
+
const seq = (workspaceCollaborationSequences.get(key) || 0) + 1;
|
|
5708
|
+
workspaceCollaborationSequences.set(key, seq);
|
|
5709
|
+
const payload = JSON.stringify({ seq, at: new Date().toISOString(), ...event });
|
|
5710
|
+
const subscribers = workspaceCollaborationSubscribers.get(key);
|
|
5711
|
+
if (!subscribers?.size) return seq;
|
|
5712
|
+
const chunk = `id: ${seq}\ndata: ${payload}\n\n`;
|
|
5713
|
+
for (const clientRes of subscribers) {
|
|
5714
|
+
try { clientRes.write(chunk); } catch (_) {}
|
|
5715
|
+
}
|
|
5716
|
+
return seq;
|
|
5717
|
+
}
|
|
5718
|
+
|
|
5628
5719
|
const WORKSPACE_SCHEDULES_FILENAME = "workspace-schedules.json";
|
|
5629
5720
|
|
|
5721
|
+
const WORKSPACE_DEFERRED_RUNS_FILENAME = "workspace-deferred-runs.json";
|
|
5722
|
+
|
|
5630
5723
|
export const WORKSPACE_SCHEDULE_POLL_MS = 30_000;
|
|
5631
5724
|
|
|
5725
|
+
export const WORKSPACE_DEFERRED_RUN_POLL_MS = 1_000;
|
|
5726
|
+
|
|
5727
|
+
const WORKSPACE_DEFERRED_LEASE_MS = 60_000;
|
|
5728
|
+
|
|
5729
|
+
const workspaceDeferredLeaseOwner = `${process.pid}-${crypto.randomBytes(8).toString("hex")}`;
|
|
5730
|
+
|
|
5731
|
+
const activeWorkspaceDeferredRuns = new Set();
|
|
5732
|
+
|
|
5632
5733
|
const WORKSPACE_IMPLEMENTATION_REFERENCE_ENABLED = true;
|
|
5633
5734
|
|
|
5634
5735
|
const WORKSPACE_IMPLEMENTATION_SUMMARY_ENABLED = false;
|
|
@@ -5694,6 +5795,123 @@ export function workspaceActiveRunsForScope(scopeKey) {
|
|
|
5694
5795
|
.filter(([, entry]) => String(entry?.scopeKey || "") === key);
|
|
5695
5796
|
}
|
|
5696
5797
|
|
|
5798
|
+
function workspaceDeferredRunsPath() {
|
|
5799
|
+
return path.join(getAgentflowDataRoot(), WORKSPACE_DEFERRED_RUNS_FILENAME);
|
|
5800
|
+
}
|
|
5801
|
+
|
|
5802
|
+
export function readWorkspaceDeferredRunRegistry() {
|
|
5803
|
+
const filePath = workspaceDeferredRunsPath();
|
|
5804
|
+
if (!fs.existsSync(filePath)) return { version: 1, runs: {} };
|
|
5805
|
+
try {
|
|
5806
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
5807
|
+
return {
|
|
5808
|
+
version: 1,
|
|
5809
|
+
runs: parsed?.runs && typeof parsed.runs === "object" && !Array.isArray(parsed.runs)
|
|
5810
|
+
? parsed.runs
|
|
5811
|
+
: {},
|
|
5812
|
+
};
|
|
5813
|
+
} catch {
|
|
5814
|
+
return { version: 1, runs: {} };
|
|
5815
|
+
}
|
|
5816
|
+
}
|
|
5817
|
+
|
|
5818
|
+
function writeWorkspaceDeferredRunRegistry(registry) {
|
|
5819
|
+
const filePath = workspaceDeferredRunsPath();
|
|
5820
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
5821
|
+
const tempPath = `${filePath}.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`;
|
|
5822
|
+
fs.writeFileSync(tempPath, JSON.stringify({
|
|
5823
|
+
version: 1,
|
|
5824
|
+
updatedAt: new Date().toISOString(),
|
|
5825
|
+
runs: registry?.runs && typeof registry.runs === "object" ? registry.runs : {},
|
|
5826
|
+
}, null, 2) + "\n", { encoding: "utf-8", mode: 0o600 });
|
|
5827
|
+
fs.renameSync(tempPath, filePath);
|
|
5828
|
+
}
|
|
5829
|
+
|
|
5830
|
+
function workspaceDeferredRunKey(meta = {}, deferred = {}) {
|
|
5831
|
+
return crypto.createHash("sha256").update([
|
|
5832
|
+
String(meta.scopeKey || ""),
|
|
5833
|
+
String(meta.runId || ""),
|
|
5834
|
+
String(deferred.nodeId || meta.nodeId || ""),
|
|
5835
|
+
].join("\n")).digest("hex").slice(0, 32);
|
|
5836
|
+
}
|
|
5837
|
+
|
|
5838
|
+
export function upsertWorkspaceDeferredRun(meta = {}, deferred = {}) {
|
|
5839
|
+
const registry = readWorkspaceDeferredRunRegistry();
|
|
5840
|
+
const key = String(meta.deferredKey || "").trim() || workspaceDeferredRunKey(meta, deferred);
|
|
5841
|
+
const previous = registry.runs?.[key] && typeof registry.runs[key] === "object" ? registry.runs[key] : {};
|
|
5842
|
+
const now = Date.now();
|
|
5843
|
+
const next = {
|
|
5844
|
+
...previous,
|
|
5845
|
+
key,
|
|
5846
|
+
kind: String(deferred.kind || previous.kind || "jenkins"),
|
|
5847
|
+
status: "waiting",
|
|
5848
|
+
scopeKey: String(meta.scopeKey || previous.scopeKey || ""),
|
|
5849
|
+
runId: String(meta.runId || previous.runId || ""),
|
|
5850
|
+
userId: String(meta.userId || previous.userId || ""),
|
|
5851
|
+
username: String(meta.username || previous.username || meta.userId || ""),
|
|
5852
|
+
flowId: String(meta.flowId || previous.flowId || ""),
|
|
5853
|
+
flowSource: String(meta.flowSource || previous.flowSource || "user"),
|
|
5854
|
+
archived: meta.archived === true || previous.archived === true,
|
|
5855
|
+
runNodeId: String(meta.runNodeId || previous.runNodeId || ""),
|
|
5856
|
+
nodeId: String(deferred.nodeId || meta.nodeId || previous.nodeId || ""),
|
|
5857
|
+
label: String(meta.label || previous.label || "Workspace Run"),
|
|
5858
|
+
plannedNodeIds: Array.isArray(meta.plannedNodeIds) ? meta.plannedNodeIds.map(String) : (previous.plannedNodeIds || []),
|
|
5859
|
+
startedAt: Number(meta.startedAt || previous.startedAt || now),
|
|
5860
|
+
scheduled: meta.scheduled === true || previous.scheduled === true,
|
|
5861
|
+
scheduleKey: String(meta.scheduleKey || previous.scheduleKey || ""),
|
|
5862
|
+
scheduleNodeId: String(meta.scheduleNodeId || previous.scheduleNodeId || ""),
|
|
5863
|
+
wakeAt: String(deferred.wakeAt || previous.wakeAt || new Date(now + WORKSPACE_DEFERRED_RUN_POLL_MS).toISOString()),
|
|
5864
|
+
phase: String(deferred.phase || previous.phase || ""),
|
|
5865
|
+
jenkinsStatus: String(deferred.status || previous.jenkinsStatus || ""),
|
|
5866
|
+
message: String(deferred.message || previous.message || ""),
|
|
5867
|
+
buildNumber: String(deferred.buildNumber || previous.buildNumber || ""),
|
|
5868
|
+
url: String(deferred.url || previous.url || ""),
|
|
5869
|
+
qrUrl: String(deferred.qrUrl || previous.qrUrl || ""),
|
|
5870
|
+
createdAt: String(previous.createdAt || new Date(now).toISOString()),
|
|
5871
|
+
updatedAt: new Date(now).toISOString(),
|
|
5872
|
+
leaseOwner: "",
|
|
5873
|
+
leaseUntil: 0,
|
|
5874
|
+
};
|
|
5875
|
+
registry.runs[key] = next;
|
|
5876
|
+
writeWorkspaceDeferredRunRegistry(registry);
|
|
5877
|
+
return next;
|
|
5878
|
+
}
|
|
5879
|
+
|
|
5880
|
+
export function removeWorkspaceDeferredRun(key) {
|
|
5881
|
+
const id = String(key || "").trim();
|
|
5882
|
+
if (!id) return null;
|
|
5883
|
+
const registry = readWorkspaceDeferredRunRegistry();
|
|
5884
|
+
const current = registry.runs?.[id] || null;
|
|
5885
|
+
if (!current) return null;
|
|
5886
|
+
delete registry.runs[id];
|
|
5887
|
+
writeWorkspaceDeferredRunRegistry(registry);
|
|
5888
|
+
return current;
|
|
5889
|
+
}
|
|
5890
|
+
|
|
5891
|
+
export function workspaceDeferredRunsForScope(scopeKey) {
|
|
5892
|
+
const key = String(scopeKey || "");
|
|
5893
|
+
return Object.values(readWorkspaceDeferredRunRegistry().runs || {})
|
|
5894
|
+
.filter((entry) => String(entry?.scopeKey || "") === key);
|
|
5895
|
+
}
|
|
5896
|
+
|
|
5897
|
+
function claimWorkspaceDeferredRun(key, now = Date.now()) {
|
|
5898
|
+
const registry = readWorkspaceDeferredRunRegistry();
|
|
5899
|
+
const entry = registry.runs?.[key];
|
|
5900
|
+
if (!entry) return null;
|
|
5901
|
+
const leaseUntil = Number(entry.leaseUntil || 0);
|
|
5902
|
+
if (leaseUntil > now && String(entry.leaseOwner || "") !== workspaceDeferredLeaseOwner) return null;
|
|
5903
|
+
const claimed = {
|
|
5904
|
+
...entry,
|
|
5905
|
+
status: "polling",
|
|
5906
|
+
leaseOwner: workspaceDeferredLeaseOwner,
|
|
5907
|
+
leaseUntil: now + WORKSPACE_DEFERRED_LEASE_MS,
|
|
5908
|
+
updatedAt: new Date(now).toISOString(),
|
|
5909
|
+
};
|
|
5910
|
+
registry.runs[key] = claimed;
|
|
5911
|
+
writeWorkspaceDeferredRunRegistry(registry);
|
|
5912
|
+
return claimed;
|
|
5913
|
+
}
|
|
5914
|
+
|
|
5697
5915
|
export function workspaceRunPlanNodeIds(runNodeId, plan) {
|
|
5698
5916
|
return Array.from(new Set([
|
|
5699
5917
|
String(runNodeId || "").trim(),
|
|
@@ -5714,6 +5932,14 @@ export function workspaceFindActiveRunConflict(scopeKey, plannedNodeIds) {
|
|
|
5714
5932
|
.filter((id) => id && planned.has(id));
|
|
5715
5933
|
if (conflictNodeIds.length) return { key, entry, conflictNodeIds };
|
|
5716
5934
|
}
|
|
5935
|
+
for (const entry of workspaceDeferredRunsForScope(scopeKey)) {
|
|
5936
|
+
const waitingIds = Array.isArray(entry?.plannedNodeIds) ? entry.plannedNodeIds : [];
|
|
5937
|
+
if (!waitingIds.length) return { key: entry.key, entry, conflictNodeIds: [] };
|
|
5938
|
+
const conflictNodeIds = waitingIds
|
|
5939
|
+
.map((id) => String(id || "").trim())
|
|
5940
|
+
.filter((id) => id && planned.has(id));
|
|
5941
|
+
if (conflictNodeIds.length) return { key: entry.key, entry, conflictNodeIds };
|
|
5942
|
+
}
|
|
5717
5943
|
return null;
|
|
5718
5944
|
}
|
|
5719
5945
|
|
|
@@ -6151,11 +6377,33 @@ export async function runWorkspaceScheduledEntry(root, entry) {
|
|
|
6151
6377
|
signal: controller.signal,
|
|
6152
6378
|
onActiveChild: setActiveChild,
|
|
6153
6379
|
onEvent: (event) => appendWorkspaceRunLogEvent(runLog.runId, event),
|
|
6380
|
+
runId,
|
|
6154
6381
|
});
|
|
6155
6382
|
const currentGraph = readWorkspaceGraph(scoped.root, root).graph;
|
|
6156
6383
|
const touchedIds = workspaceRunTouchedNodeIds(result);
|
|
6157
6384
|
const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
|
|
6158
6385
|
writeWorkspaceGraph(scoped.root, mergedGraph, root);
|
|
6386
|
+
if (result.deferred) {
|
|
6387
|
+
const waiting = upsertWorkspaceDeferredRun({
|
|
6388
|
+
...runEntry,
|
|
6389
|
+
scheduleKey: entry.key,
|
|
6390
|
+
scheduleNodeId,
|
|
6391
|
+
}, result.deferred);
|
|
6392
|
+
appendWorkspaceRunLogEvent(runLog.runId, {
|
|
6393
|
+
type: "run-waiting",
|
|
6394
|
+
nodeId: waiting.nodeId,
|
|
6395
|
+
wakeAt: waiting.wakeAt,
|
|
6396
|
+
phase: waiting.phase,
|
|
6397
|
+
jenkinsStatus: waiting.jenkinsStatus,
|
|
6398
|
+
ts: Date.now(),
|
|
6399
|
+
});
|
|
6400
|
+
updateWorkspaceScheduleEntry(entry.key, {
|
|
6401
|
+
nextRunAt: computeNext(config),
|
|
6402
|
+
lastStatus: "waiting",
|
|
6403
|
+
lastError: "",
|
|
6404
|
+
});
|
|
6405
|
+
return;
|
|
6406
|
+
}
|
|
6159
6407
|
const endedAt = Date.now();
|
|
6160
6408
|
appendWorkspaceRunFinished({ ...runEntry, endedAt, durationMs: endedAt - runEntry.startedAt }, "success");
|
|
6161
6409
|
finishWorkspaceRunLogSession(runLog.runId, "success", {
|
|
@@ -6197,3 +6445,157 @@ export async function runWorkspaceScheduledEntry(root, entry) {
|
|
|
6197
6445
|
if (activeWorkspaceRuns.get(runKey) === runEntry) activeWorkspaceRuns.delete(runKey);
|
|
6198
6446
|
}
|
|
6199
6447
|
}
|
|
6448
|
+
|
|
6449
|
+
function finishWorkspaceDeferredRun(entry, status, patch = {}) {
|
|
6450
|
+
const endedAt = Number(patch.endedAt || Date.now());
|
|
6451
|
+
appendWorkspaceRunFinished({
|
|
6452
|
+
...entry,
|
|
6453
|
+
endedAt,
|
|
6454
|
+
durationMs: Math.max(0, endedAt - Number(entry.startedAt || endedAt)),
|
|
6455
|
+
}, status);
|
|
6456
|
+
finishWorkspaceRunLogSession(entry.runId, status, {
|
|
6457
|
+
endedAt,
|
|
6458
|
+
durationMs: Math.max(0, endedAt - Number(entry.startedAt || endedAt)),
|
|
6459
|
+
runNodeId: entry.runNodeId || "",
|
|
6460
|
+
error: String(patch.error || ""),
|
|
6461
|
+
});
|
|
6462
|
+
if (entry.scheduleKey) {
|
|
6463
|
+
updateWorkspaceScheduleEntry(entry.scheduleKey, {
|
|
6464
|
+
lastFinishedAt: endedAt,
|
|
6465
|
+
lastStatus: status,
|
|
6466
|
+
lastError: String(patch.error || ""),
|
|
6467
|
+
...(patch.error ? { lastErrorAt: endedAt } : {}),
|
|
6468
|
+
});
|
|
6469
|
+
}
|
|
6470
|
+
}
|
|
6471
|
+
|
|
6472
|
+
async function runWorkspaceDeferredEntry(root, claimed) {
|
|
6473
|
+
const userCtx = { userId: String(claimed.userId || "") };
|
|
6474
|
+
const scoped = resolveWorkspaceScopeRoot(root, {
|
|
6475
|
+
flowId: claimed.flowId || "",
|
|
6476
|
+
flowSource: claimed.flowSource || "user",
|
|
6477
|
+
archived: claimed.archived === true,
|
|
6478
|
+
}, userCtx);
|
|
6479
|
+
if (scoped.error || scoped.archived || isReadonlyBuiltinFlowSource(scoped.flowSource)) {
|
|
6480
|
+
const error = scoped.error || "Deferred Workspace target is not writable";
|
|
6481
|
+
removeWorkspaceDeferredRun(claimed.key);
|
|
6482
|
+
appendWorkspaceRunLogEvent(claimed.runId, { type: "error", error, ts: Date.now() });
|
|
6483
|
+
finishWorkspaceDeferredRun(claimed, "failed", { error });
|
|
6484
|
+
return;
|
|
6485
|
+
}
|
|
6486
|
+
|
|
6487
|
+
const controller = new AbortController();
|
|
6488
|
+
const runControl = workspaceRunControl(controller);
|
|
6489
|
+
const runKey = workspaceRunEntryKey(claimed.scopeKey, claimed.runId);
|
|
6490
|
+
const runEntry = {
|
|
6491
|
+
...claimed,
|
|
6492
|
+
controller,
|
|
6493
|
+
runControl,
|
|
6494
|
+
plannedNodeIds: Array.isArray(claimed.plannedNodeIds) ? claimed.plannedNodeIds : [],
|
|
6495
|
+
};
|
|
6496
|
+
activeWorkspaceRuns.set(runKey, runEntry);
|
|
6497
|
+
let activeReleased = false;
|
|
6498
|
+
const releaseActive = (status = "finished") => {
|
|
6499
|
+
if (activeReleased) return;
|
|
6500
|
+
activeReleased = true;
|
|
6501
|
+
runControl.finish(status);
|
|
6502
|
+
if (activeWorkspaceRuns.get(runKey) === runEntry) activeWorkspaceRuns.delete(runKey);
|
|
6503
|
+
};
|
|
6504
|
+
try {
|
|
6505
|
+
const graph = hydrateWorkspaceGraphForRuntime(root, scoped, readWorkspaceGraph(scoped.root, root).graph, userCtx);
|
|
6506
|
+
const result = await runWorkspaceGraph(root, scoped.root, {
|
|
6507
|
+
flowId: claimed.flowId,
|
|
6508
|
+
flowSource: claimed.flowSource || "user",
|
|
6509
|
+
runNodeId: claimed.runNodeId,
|
|
6510
|
+
graph,
|
|
6511
|
+
}, userCtx, {
|
|
6512
|
+
signal: controller.signal,
|
|
6513
|
+
onActiveChild: (child, options = {}) => runControl.setChild(child, options),
|
|
6514
|
+
onEvent: (event) => appendWorkspaceRunLogEvent(claimed.runId, event),
|
|
6515
|
+
runId: claimed.runId,
|
|
6516
|
+
});
|
|
6517
|
+
const currentGraph = readWorkspaceGraph(scoped.root, root).graph;
|
|
6518
|
+
const touchedIds = workspaceRunTouchedNodeIds(result);
|
|
6519
|
+
const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
|
|
6520
|
+
writeWorkspaceGraph(scoped.root, mergedGraph, root);
|
|
6521
|
+
if (result.deferred) {
|
|
6522
|
+
const waiting = upsertWorkspaceDeferredRun({ ...claimed, deferredKey: claimed.key }, result.deferred);
|
|
6523
|
+
appendWorkspaceRunLogEvent(claimed.runId, {
|
|
6524
|
+
type: "run-waiting",
|
|
6525
|
+
nodeId: waiting.nodeId,
|
|
6526
|
+
wakeAt: waiting.wakeAt,
|
|
6527
|
+
phase: waiting.phase,
|
|
6528
|
+
jenkinsStatus: waiting.jenkinsStatus,
|
|
6529
|
+
ts: Date.now(),
|
|
6530
|
+
});
|
|
6531
|
+
if (claimed.scheduleKey) updateWorkspaceScheduleEntry(claimed.scheduleKey, { lastStatus: "waiting" });
|
|
6532
|
+
releaseActive("waiting");
|
|
6533
|
+
emitWorkspaceCollaborationEvent(userCtx, scoped.flowSource, scoped.flowId, scoped.archived, {
|
|
6534
|
+
type: "runtime.committed",
|
|
6535
|
+
runId: claimed.runId,
|
|
6536
|
+
runNodeId: claimed.runNodeId,
|
|
6537
|
+
actorId: userCtx.userId || "",
|
|
6538
|
+
source: "deferred-run",
|
|
6539
|
+
});
|
|
6540
|
+
emitWorkspaceCollaborationEvent(userCtx, scoped.flowSource, scoped.flowId, scoped.archived, {
|
|
6541
|
+
type: "run.waiting",
|
|
6542
|
+
status: "waiting",
|
|
6543
|
+
runId: claimed.runId,
|
|
6544
|
+
runNodeId: claimed.runNodeId,
|
|
6545
|
+
actorId: userCtx.userId || "",
|
|
6546
|
+
});
|
|
6547
|
+
return;
|
|
6548
|
+
}
|
|
6549
|
+
|
|
6550
|
+
removeWorkspaceDeferredRun(claimed.key);
|
|
6551
|
+
finishWorkspaceDeferredRun(claimed, "success");
|
|
6552
|
+
releaseActive("finished");
|
|
6553
|
+
emitWorkspaceCollaborationEvent(userCtx, scoped.flowSource, scoped.flowId, scoped.archived, {
|
|
6554
|
+
type: "runtime.committed",
|
|
6555
|
+
runId: claimed.runId,
|
|
6556
|
+
runNodeId: claimed.runNodeId,
|
|
6557
|
+
actorId: userCtx.userId || "",
|
|
6558
|
+
source: "deferred-run",
|
|
6559
|
+
});
|
|
6560
|
+
emitWorkspaceCollaborationEvent(userCtx, scoped.flowSource, scoped.flowId, scoped.archived, {
|
|
6561
|
+
type: "run.finished",
|
|
6562
|
+
status: "success",
|
|
6563
|
+
runId: claimed.runId,
|
|
6564
|
+
runNodeId: claimed.runNodeId,
|
|
6565
|
+
actorId: userCtx.userId || "",
|
|
6566
|
+
});
|
|
6567
|
+
} catch (e) {
|
|
6568
|
+
const error = (e && e.message) || String(e);
|
|
6569
|
+
const stopped = isWorkspaceRunAbortError(e) || controller.signal.aborted;
|
|
6570
|
+
removeWorkspaceDeferredRun(claimed.key);
|
|
6571
|
+
appendWorkspaceRunLogEvent(claimed.runId, stopped
|
|
6572
|
+
? { type: "stopped", message: "Workspace run stopped", ts: Date.now() }
|
|
6573
|
+
: { type: "error", error, ts: Date.now() });
|
|
6574
|
+
finishWorkspaceDeferredRun(claimed, stopped ? "stopped" : "failed", { error: stopped ? "" : error });
|
|
6575
|
+
releaseActive(stopped ? "stopped" : "failed");
|
|
6576
|
+
emitWorkspaceCollaborationEvent(userCtx, scoped.flowSource, scoped.flowId, scoped.archived, {
|
|
6577
|
+
type: "run.finished",
|
|
6578
|
+
status: stopped ? "stopped" : "failed",
|
|
6579
|
+
runId: claimed.runId,
|
|
6580
|
+
runNodeId: claimed.runNodeId,
|
|
6581
|
+
actorId: userCtx.userId || "",
|
|
6582
|
+
});
|
|
6583
|
+
if (!stopped) log.info(`[workspace-deferred] failed ${claimed.flowId}/${claimed.runNodeId}: ${error}`);
|
|
6584
|
+
} finally {
|
|
6585
|
+
releaseActive(controller.signal.aborted ? "stopped" : "finished");
|
|
6586
|
+
}
|
|
6587
|
+
}
|
|
6588
|
+
|
|
6589
|
+
export function pollWorkspaceDeferredRuns(root, now = Date.now()) {
|
|
6590
|
+
const registry = readWorkspaceDeferredRunRegistry();
|
|
6591
|
+
for (const entry of Object.values(registry.runs || {})) {
|
|
6592
|
+
const key = String(entry?.key || "").trim();
|
|
6593
|
+
if (!key || activeWorkspaceDeferredRuns.has(key)) continue;
|
|
6594
|
+
const wakeAt = Date.parse(String(entry.wakeAt || ""));
|
|
6595
|
+
if (Number.isFinite(wakeAt) && wakeAt > now) continue;
|
|
6596
|
+
const claimed = claimWorkspaceDeferredRun(key, now);
|
|
6597
|
+
if (!claimed) continue;
|
|
6598
|
+
activeWorkspaceDeferredRuns.add(key);
|
|
6599
|
+
void runWorkspaceDeferredEntry(root, claimed).finally(() => activeWorkspaceDeferredRuns.delete(key));
|
|
6600
|
+
}
|
|
6601
|
+
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
# Built-in node: durable Jenkins build
|
|
3
|
-
runtime:
|
|
4
|
-
palette: hidden
|
|
3
|
+
runtime: native
|
|
5
4
|
description: |
|
|
6
5
|
Trigger one Jenkins job and durably monitor it until completion.
|
|
7
6
|
|
|
8
|
-
The node persists queue/build checkpoints and
|
|
9
|
-
|
|
7
|
+
The node persists queue/build checkpoints and uses server-side deferred polling at `pollInterval`:
|
|
8
|
+
the Workspace request returns in a waiting state instead of keeping an HTTP request or worker asleep.
|
|
9
|
+
The AgentFlow server registry wakes the flow again and continues downstream after completion.
|
|
10
|
+
An existing queueId/buildNumber is reused after a run or AgentFlow server interruption,
|
|
10
11
|
so a resumed run does not trigger the job again. Jenkins FAILURE/ABORTED/TIMEOUT are business
|
|
11
12
|
outcomes and continue to downstream notification nodes. Authentication, configuration, and
|
|
12
13
|
repeated platform request errors fail the AgentFlow node.
|