@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.
@@ -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
- for (const id of Array.isArray(result?.order) ? result.order : []) {
1131
- const text = String(id || "").trim();
1132
- if (text) ids.add(text);
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: none
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 never keeps a process blocked while waiting.
9
- Scheduler re-enters the same node at `pollInterval`; an existing queueId/buildNumber is reused,
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.