@fieldwangai/agentflow 0.1.163 → 0.1.165

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.
@@ -42,9 +42,10 @@ import { readMergedEnvObject, runtimeEnvForUser } from "./user-env.mjs";
42
42
  import { sendWecomAppMarkdown, sendWecomGroupMarkdown } from "./wecom.mjs";
43
43
  import { getWorkspaceCollaborationByFlow, getWorkspaceCollaborationForProject, listWorkspaceCollaborationsForUser, workspaceCollaborationAccess, workspaceCollaborationSummary } from "./workspace-collaboration.mjs";
44
44
  import { FLOW_SOURCE_FILENAME, WORKSPACE_GRAPH_FILENAME, WorkspaceFlowParseError, readWorkspaceGraphFiles, readWorkspaceRunFingerprints, writeWorkspaceGraphFiles } from "./workspace-flow-store.mjs";
45
+ import { workspaceDesignRevision } from "./workspace-graph-merge.mjs";
45
46
  import { createWorkspaceRunController, terminateWorkspaceChild } from "./workspace-run-controller.mjs";
46
47
  import { appendWorkspaceRunLogEvent, createWorkspaceRunLogSession, finishWorkspaceRunLogSession } from "./workspace-run-logs.mjs";
47
- import { splitWorkspaceGraph } from "./workspace-state.mjs";
48
+ import { mergeWorkspaceState, splitWorkspaceGraph } from "./workspace-state.mjs";
48
49
  import { isWorkspaceDraftDir } from "./workspace-draft.mjs";
49
50
  import { getPipelineFiles } from "./workspace-tree.mjs";
50
51
  import { spawn } from "child_process";
@@ -708,6 +709,249 @@ export function readWorkspaceGraph(workspaceRoot, marketplaceRoot = "") {
708
709
  return { path: designPath, graph };
709
710
  }
710
711
 
712
+ const WORKSPACE_RELEASES_REL = path.join(".workspace", "agentflow", "releases");
713
+ const WORKSPACE_RELEASE_REGISTRY = "registry.json";
714
+ const WORKSPACE_RELEASE_MANIFEST = "release.json";
715
+ const WORKSPACE_RELEASE_SKIP_ROOTS = new Set([
716
+ ".git",
717
+ "node_modules",
718
+ "outputs",
719
+ "runBuild",
720
+ "workspace.state.json",
721
+ ]);
722
+
723
+ function workspaceReleasesRoot(workspaceRoot) {
724
+ return path.join(path.resolve(workspaceRoot), WORKSPACE_RELEASES_REL);
725
+ }
726
+
727
+ function workspaceReleaseRegistryPath(workspaceRoot) {
728
+ return path.join(workspaceReleasesRoot(workspaceRoot), WORKSPACE_RELEASE_REGISTRY);
729
+ }
730
+
731
+ function normalizeWorkspaceReleaseRegistry(value = {}) {
732
+ const releases = Array.isArray(value?.releases)
733
+ ? value.releases
734
+ .filter((release) => release && /^v[1-9][0-9]*$/.test(String(release.id || "")))
735
+ .map((release) => ({
736
+ id: String(release.id),
737
+ number: Math.max(1, Number(release.number || String(release.id).slice(1)) || 1),
738
+ designRevision: String(release.designRevision || ""),
739
+ createdAt: String(release.createdAt || ""),
740
+ createdBy: String(release.createdBy || ""),
741
+ notes: String(release.notes || ""),
742
+ baseReleaseId: String(release.baseReleaseId || ""),
743
+ }))
744
+ .sort((a, b) => b.number - a.number)
745
+ : [];
746
+ const stableReleaseId = releases.some((release) => release.id === value?.stableReleaseId)
747
+ ? String(value.stableReleaseId)
748
+ : "";
749
+ return {
750
+ version: 1,
751
+ stableReleaseId,
752
+ nextNumber: Math.max(
753
+ Number(value?.nextNumber || 1) || 1,
754
+ releases.reduce((max, release) => Math.max(max, release.number + 1), 1),
755
+ ),
756
+ releases,
757
+ updatedAt: String(value?.updatedAt || ""),
758
+ };
759
+ }
760
+
761
+ function readWorkspaceReleaseRegistry(workspaceRoot) {
762
+ const filePath = workspaceReleaseRegistryPath(workspaceRoot);
763
+ try {
764
+ if (!fs.existsSync(filePath)) return normalizeWorkspaceReleaseRegistry();
765
+ return normalizeWorkspaceReleaseRegistry(JSON.parse(fs.readFileSync(filePath, "utf-8")));
766
+ } catch {
767
+ return normalizeWorkspaceReleaseRegistry();
768
+ }
769
+ }
770
+
771
+ function writeWorkspaceReleaseRegistry(workspaceRoot, registry) {
772
+ const releasesRoot = workspaceReleasesRoot(workspaceRoot);
773
+ fs.mkdirSync(releasesRoot, { recursive: true });
774
+ const filePath = workspaceReleaseRegistryPath(workspaceRoot);
775
+ const tempPath = path.join(releasesRoot, `.registry-${crypto.randomUUID()}.tmp`);
776
+ const normalized = normalizeWorkspaceReleaseRegistry({
777
+ ...registry,
778
+ updatedAt: new Date().toISOString(),
779
+ });
780
+ fs.writeFileSync(tempPath, `${JSON.stringify(normalized, null, 2)}\n`, "utf-8");
781
+ fs.renameSync(tempPath, filePath);
782
+ return normalized;
783
+ }
784
+
785
+ function workspaceReleaseSnapshotRoot(workspaceRoot, releaseId) {
786
+ const id = String(releaseId || "").trim();
787
+ if (!/^v[1-9][0-9]*$/.test(id)) return "";
788
+ return path.join(workspaceReleasesRoot(workspaceRoot), id, "snapshot");
789
+ }
790
+
791
+ function workspaceReleaseRuntimeStatePath(workspaceRoot, releaseId) {
792
+ const id = String(releaseId || "").trim();
793
+ if (!/^v[1-9][0-9]*$/.test(id)) return "";
794
+ return path.join(workspaceReleasesRoot(workspaceRoot), id, "runtime", "workspace.state.json");
795
+ }
796
+
797
+ function readWorkspaceReleaseRuntimeState(workspaceRoot, releaseId) {
798
+ const filePath = workspaceReleaseRuntimeStatePath(workspaceRoot, releaseId);
799
+ if (!filePath || !fs.existsSync(filePath)) return null;
800
+ try {
801
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
802
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
803
+ } catch {
804
+ return null;
805
+ }
806
+ }
807
+
808
+ function writeWorkspaceReleaseRuntimeState(workspaceRoot, releaseId, graph) {
809
+ const filePath = workspaceReleaseRuntimeStatePath(workspaceRoot, releaseId);
810
+ if (!filePath) return;
811
+ const { state } = splitWorkspaceGraph(graph || {});
812
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
813
+ const tempPath = `${filePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
814
+ fs.writeFileSync(tempPath, `${JSON.stringify(state || { version: 1 }, null, 2)}\n`, { encoding: "utf-8", mode: 0o600 });
815
+ fs.renameSync(tempPath, filePath);
816
+ }
817
+
818
+ function workspaceCopyReleaseSnapshot(sourceRoot, targetRoot, relative = "") {
819
+ const source = relative ? path.join(sourceRoot, relative) : sourceRoot;
820
+ const entries = fs.readdirSync(source, { withFileTypes: true });
821
+ fs.mkdirSync(relative ? path.join(targetRoot, relative) : targetRoot, { recursive: true });
822
+ for (const entry of entries) {
823
+ const nextRelative = relative ? path.join(relative, entry.name) : entry.name;
824
+ const normalized = nextRelative.replace(/\\/g, "/");
825
+ if (!relative && WORKSPACE_RELEASE_SKIP_ROOTS.has(entry.name)) continue;
826
+ if (normalized === ".workspace/agentflow" || normalized.startsWith(".workspace/agentflow/")) continue;
827
+ const sourcePath = path.join(sourceRoot, nextRelative);
828
+ const targetPath = path.join(targetRoot, nextRelative);
829
+ if (entry.isSymbolicLink()) continue;
830
+ if (entry.isDirectory()) {
831
+ workspaceCopyReleaseSnapshot(sourceRoot, targetRoot, nextRelative);
832
+ } else if (entry.isFile()) {
833
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
834
+ try {
835
+ fs.copyFileSync(sourcePath, targetPath, fs.constants.COPYFILE_FICLONE);
836
+ } catch {
837
+ fs.copyFileSync(sourcePath, targetPath);
838
+ }
839
+ }
840
+ }
841
+ }
842
+
843
+ function workspaceReleaseSummary(workspaceRoot, marketplaceRoot = "", graph = null) {
844
+ const registry = readWorkspaceReleaseRegistry(workspaceRoot);
845
+ const currentGraph = graph || readWorkspaceGraph(workspaceRoot, marketplaceRoot).graph;
846
+ const draftRevision = workspaceDesignRevision(currentGraph);
847
+ const stable = registry.releases.find((release) => release.id === registry.stableReleaseId) || null;
848
+ return {
849
+ enabled: Boolean(stable),
850
+ stableReleaseId: stable?.id || "",
851
+ stableRevision: stable?.designRevision || "",
852
+ draftRevision,
853
+ hasDraftChanges: Boolean(stable && stable.designRevision !== draftRevision),
854
+ releases: registry.releases,
855
+ };
856
+ }
857
+
858
+ export function readWorkspaceReleaseStatus(workspaceRoot, marketplaceRoot = "", graph = null) {
859
+ return workspaceReleaseSummary(workspaceRoot, marketplaceRoot, graph);
860
+ }
861
+
862
+ export function readWorkspaceStableRelease(workspaceRoot, marketplaceRoot = "") {
863
+ const registry = readWorkspaceReleaseRegistry(workspaceRoot);
864
+ const release = registry.releases.find((item) => item.id === registry.stableReleaseId) || null;
865
+ if (!release) return null;
866
+ const root = workspaceReleaseSnapshotRoot(workspaceRoot, release.id);
867
+ if (!root || !fs.existsSync(root)) return null;
868
+ const designGraph = readWorkspaceGraph(root, marketplaceRoot).graph;
869
+ const graph = mergeWorkspaceState(
870
+ designGraph,
871
+ readWorkspaceReleaseRuntimeState(workspaceRoot, release.id),
872
+ );
873
+ return { release, root, graph };
874
+ }
875
+
876
+ export function publishWorkspaceRelease(workspaceRoot, marketplaceRoot = "", options = {}) {
877
+ const graph = readWorkspaceGraph(workspaceRoot, marketplaceRoot).graph;
878
+ const designRevision = workspaceDesignRevision(graph);
879
+ const expectedRevision = String(options.expectedRevision || "").trim();
880
+ if (expectedRevision && expectedRevision !== designRevision) {
881
+ return {
882
+ error: "Workspace 已更新,请保存并刷新后再发布",
883
+ conflict: "revision-mismatch",
884
+ expectedRevision,
885
+ currentRevision: designRevision,
886
+ };
887
+ }
888
+ const registry = readWorkspaceReleaseRegistry(workspaceRoot);
889
+ const number = registry.nextNumber;
890
+ const releaseId = `v${number}`;
891
+ const releasesRoot = workspaceReleasesRoot(workspaceRoot);
892
+ const releaseRoot = path.join(releasesRoot, releaseId);
893
+ const snapshotRoot = path.join(releaseRoot, "snapshot");
894
+ const tempRoot = path.join(releasesRoot, `.publish-${releaseId}-${crypto.randomUUID()}`);
895
+ const tempSnapshotRoot = path.join(tempRoot, "snapshot");
896
+ const now = new Date().toISOString();
897
+ const release = {
898
+ id: releaseId,
899
+ number,
900
+ designRevision,
901
+ createdAt: now,
902
+ createdBy: String(options.createdBy || ""),
903
+ notes: String(options.notes || "").trim().slice(0, 2000),
904
+ baseReleaseId: registry.stableReleaseId || "",
905
+ };
906
+ let releaseInstalled = false;
907
+ let registryCommitted = false;
908
+ fs.mkdirSync(releasesRoot, { recursive: true });
909
+ try {
910
+ workspaceCopyReleaseSnapshot(path.resolve(workspaceRoot), tempSnapshotRoot);
911
+ const { design } = splitWorkspaceGraph(graph);
912
+ writeWorkspaceGraph(tempSnapshotRoot, design, marketplaceRoot);
913
+ fs.writeFileSync(path.join(tempRoot, WORKSPACE_RELEASE_MANIFEST), `${JSON.stringify(release, null, 2)}\n`, "utf-8");
914
+ if (fs.existsSync(releaseRoot)) throw new Error(`Release already exists: ${releaseId}`);
915
+ fs.renameSync(tempRoot, releaseRoot);
916
+ releaseInstalled = true;
917
+ const nextRegistry = writeWorkspaceReleaseRegistry(workspaceRoot, {
918
+ ...registry,
919
+ stableReleaseId: releaseId,
920
+ nextNumber: number + 1,
921
+ releases: [release, ...registry.releases],
922
+ });
923
+ registryCommitted = true;
924
+ return {
925
+ ok: true,
926
+ release,
927
+ status: workspaceReleaseSummary(workspaceRoot, marketplaceRoot, graph),
928
+ registry: nextRegistry,
929
+ snapshotRoot,
930
+ };
931
+ } catch (error) {
932
+ try { fs.rmSync(tempRoot, { recursive: true, force: true }); } catch {}
933
+ if (releaseInstalled && !registryCommitted) {
934
+ try { fs.rmSync(releaseRoot, { recursive: true, force: true }); } catch {}
935
+ }
936
+ throw error;
937
+ }
938
+ }
939
+
940
+ export function rollbackWorkspaceRelease(workspaceRoot, releaseId, marketplaceRoot = "") {
941
+ const registry = readWorkspaceReleaseRegistry(workspaceRoot);
942
+ const release = registry.releases.find((item) => item.id === String(releaseId || "").trim()) || null;
943
+ const snapshotRoot = release ? workspaceReleaseSnapshotRoot(workspaceRoot, release.id) : "";
944
+ if (!release || !snapshotRoot || !fs.existsSync(snapshotRoot)) {
945
+ return { error: "Release not found" };
946
+ }
947
+ writeWorkspaceReleaseRegistry(workspaceRoot, { ...registry, stableReleaseId: release.id });
948
+ return {
949
+ ok: true,
950
+ release,
951
+ status: workspaceReleaseSummary(workspaceRoot, marketplaceRoot),
952
+ };
953
+ }
954
+
711
955
  const DISPLAY_SHARE_FILENAME = "display-shares.json";
712
956
 
713
957
  const DISPLAY_SHARE_ALLOWED_EXPIRY_DAYS = new Set([1, 7, 30, 90, 365]);
@@ -1252,6 +1496,24 @@ export function mergeWorkspaceRunGraph(currentGraph, runGraph, touchedIds) {
1252
1496
  };
1253
1497
  }
1254
1498
 
1499
+ function mergeWorkspaceRunState(currentGraph, runGraph, touchedIds) {
1500
+ const currentSplit = splitWorkspaceGraph(currentGraph || {});
1501
+ const runSplit = splitWorkspaceGraph(runGraph || {});
1502
+ const ids = touchedIds instanceof Set ? touchedIds : new Set(touchedIds || []);
1503
+ const state = JSON.parse(JSON.stringify(currentSplit.state || { version: 1 }));
1504
+ for (const key of ["inputs", "outputs", "displayBodies", "displayReloadKeys", "fingerprints"]) {
1505
+ const currentBucket = state[key] && typeof state[key] === "object" ? state[key] : {};
1506
+ const runBucket = runSplit.state?.[key] && typeof runSplit.state[key] === "object" ? runSplit.state[key] : {};
1507
+ for (const nodeId of ids) {
1508
+ if (Object.prototype.hasOwnProperty.call(runBucket, nodeId)) currentBucket[nodeId] = runBucket[nodeId];
1509
+ else delete currentBucket[nodeId];
1510
+ }
1511
+ if (Object.keys(currentBucket).length) state[key] = currentBucket;
1512
+ else delete state[key];
1513
+ }
1514
+ return mergeWorkspaceState(currentSplit.design, state);
1515
+ }
1516
+
1255
1517
  export function mergeWorkspacePersistentNodeRefs(incomingGraph, currentGraph) {
1256
1518
  const incoming = normalizeWorkspaceGraphPayload(incomingGraph || {});
1257
1519
  const current = normalizeWorkspaceGraphPayload(currentGraph || {});
@@ -1489,30 +1751,45 @@ export function resolveWorkspaceScopeRoot(workspaceRoot, params = {}, opts = {})
1489
1751
  if (opts.isAdmin !== true) {
1490
1752
  return { root: "", error: "Admin permission required", status: 403 };
1491
1753
  }
1492
- if (flowSource !== "user") {
1493
- return { root: "", error: "Admin read-only review only supports user projects", status: 400 };
1754
+ if (!["user", "workspace"].includes(flowSource)) {
1755
+ return { root: "", error: "Admin read-only review only supports user or shared Workspaces", status: 400 };
1494
1756
  }
1495
- const owner = adminWorkspaceOwnerSummary(adminOwnerId);
1757
+ const collaboration = flowSource === "workspace"
1758
+ ? getWorkspaceCollaborationByFlow(flowId, archived)
1759
+ : null;
1760
+ const owner = adminWorkspaceOwnerSummary(collaboration?.ownerId || adminOwnerId);
1496
1761
  if (!owner) {
1497
1762
  return { root: "", error: "Workspace owner not found", status: 404 };
1498
1763
  }
1499
- const targetFlow = listFlowsJson(workspaceRoot, { userId: owner.userId })
1500
- .find((flow) => (
1501
- flow.id === flowId
1502
- && (flow.source || "user") === "user"
1503
- && Boolean(flow.archived) === archived
1504
- ));
1505
- if (!targetFlow?.path) {
1764
+ let targetPath = "";
1765
+ let physicalFlowSource = flowSource;
1766
+ if (flowSource === "user") {
1767
+ const targetFlow = listFlowsJson(workspaceRoot, { userId: owner.userId })
1768
+ .find((flow) => (
1769
+ flow.id === flowId
1770
+ && (flow.source || "user") === "user"
1771
+ && Boolean(flow.archived) === archived
1772
+ ));
1773
+ targetPath = targetFlow?.path || "";
1774
+ } else {
1775
+ physicalFlowSource = collaboration?.projectSource || collaboration?.flowSource || "workspace";
1776
+ const result = getPipelineFiles(workspaceRoot, flowId, physicalFlowSource, archived, {
1777
+ ...opts,
1778
+ userId: owner.userId,
1779
+ });
1780
+ targetPath = result?.path || "";
1781
+ }
1782
+ if (!targetPath) {
1506
1783
  return { root: "", error: "Pipeline workspace not found", status: 404 };
1507
1784
  }
1508
1785
  return {
1509
- root: path.resolve(targetFlow.path),
1786
+ root: path.resolve(targetPath),
1510
1787
  flowId,
1511
- flowSource: "user",
1788
+ flowSource: physicalFlowSource,
1512
1789
  requestedFlowSource: flowSource,
1513
- workspaceId: "",
1790
+ workspaceId: collaboration?.id || "",
1514
1791
  archived,
1515
- collaboration: null,
1792
+ collaboration,
1516
1793
  collaborationAccess: {
1517
1794
  allowed: true,
1518
1795
  writable: false,
@@ -4668,12 +4945,13 @@ function workspaceCreateNodeTmpDir(runTmpRoot, nodeId) {
4668
4945
  return dir;
4669
4946
  }
4670
4947
 
4671
- function workspaceCreateNodeRunPackage(runTmpRoot, nodeId, { scopedRoot, cwd = "", task = "", inputValues = {}, skillsBlock = "", mcpBlock = "", resultFile = "", outParamFiles = {}, durableOutputs = false } = {}) {
4948
+ function workspaceCreateNodeRunPackage(runTmpRoot, nodeId, { scopedRoot, sourceRoot = "", cwd = "", task = "", inputValues = {}, skillsBlock = "", mcpBlock = "", resultFile = "", outParamFiles = {}, durableOutputs = false } = {}) {
4672
4949
  const nodeRunDir = workspaceCreateNodeTmpDir(runTmpRoot, nodeId);
4673
4950
  const nodeTmpDir = path.join(nodeRunDir, "tmp");
4674
4951
  const legacyOutputsDir = path.join(nodeRunDir, "outputs");
4675
- const workspaceRoot = path.resolve(scopedRoot);
4676
- const workspaceOutputsDir = path.join(workspaceRoot, "outputs");
4952
+ const workspaceRoot = path.resolve(sourceRoot || scopedRoot);
4953
+ const outputWorkspaceRoot = path.resolve(scopedRoot);
4954
+ const workspaceOutputsDir = path.join(outputWorkspaceRoot, "outputs");
4677
4955
  const nodePart = workspaceSanitizeTmpSegment(nodeId || "node", "node");
4678
4956
  const outputsRel = durableOutputs ? path.posix.join("outputs", nodePart) : "outputs";
4679
4957
  const outputsDir = durableOutputs ? path.join(workspaceOutputsDir, nodePart) : legacyOutputsDir;
@@ -4703,6 +4981,7 @@ function workspaceCreateNodeRunPackage(runTmpRoot, nodeId, { scopedRoot, cwd = "
4703
4981
  outputsRel,
4704
4982
  directWorkspaceOutputs: durableOutputs,
4705
4983
  workspaceRoot,
4984
+ outputWorkspaceRoot,
4706
4985
  workspaceOutputsDir,
4707
4986
  executionCwd: cwd ? path.resolve(cwd) : workspaceRoot,
4708
4987
  resultFileRel,
@@ -5277,22 +5556,23 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
5277
5556
  emit({ type: "status", nodeId, line: `Timing ${label}: ${elapsedMs}ms`, timing: { label, elapsedMs, ...extra } });
5278
5557
  };
5279
5558
  let cwd = scopedRoot;
5559
+ const runtimeStorageRoot = path.resolve(opts.runtimeRoot || scopedRoot);
5280
5560
  const modelKey = typeof payload?.model === "string" ? payload.model.trim() : "";
5281
5561
  const runEnv = {};
5282
5562
  const runtimeEnv = (extra = {}) => runtimeEnvForUser(userCtx, { ...runEnv, ...(extra || {}) });
5283
5563
  const autoCleanupWorktrees = [];
5284
- const runTmpRoot = workspaceCreateRunTmpRoot(scopedRoot, runNodeId);
5564
+ const runTmpRoot = workspaceCreateRunTmpRoot(runtimeStorageRoot, runNodeId);
5285
5565
  const runtimeRunId = String(opts.runId || payload?.runId || "").trim() || runLedgerId("workspace-execution");
5286
5566
  const ownsRunManifest = !(Array.isArray(opts?.subflowCallStack) && opts.subflowCallStack.length);
5287
5567
  const persistRunManifest = (status, extra = {}) => {
5288
5568
  if (!ownsRunManifest) return null;
5289
- return workspaceWriteRunManifest(scopedRoot, runtimeRunId, {
5569
+ return workspaceWriteRunManifest(runtimeStorageRoot, runtimeRunId, {
5290
5570
  flowId: String(payload?.flowId || ""),
5291
5571
  flowSource: String(payload?.flowSource || "user"),
5292
5572
  runNodeId,
5293
5573
  status,
5294
5574
  runtimeRoot: runTmpRoot,
5295
- artifactRoot: path.join(path.resolve(scopedRoot), "outputs"),
5575
+ artifactRoot: path.join(runtimeStorageRoot, "outputs"),
5296
5576
  worktrees: autoCleanupWorktrees.map((entry) => ({ ...entry, removed: false })),
5297
5577
  ...extra,
5298
5578
  });
@@ -5705,7 +5985,8 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
5705
5985
  });
5706
5986
  }
5707
5987
  const runPackage = workspaceCreateNodeRunPackage(runTmpRoot, `${nodeId}-iteration-${iteration}`, {
5708
- scopedRoot,
5988
+ scopedRoot: runtimeStorageRoot,
5989
+ sourceRoot: scopedRoot,
5709
5990
  cwd,
5710
5991
  task: stepScript || stepScriptRef,
5711
5992
  inputValues: iterationInputs,
@@ -6283,7 +6564,8 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
6283
6564
  const prepareStartedAt = Date.now();
6284
6565
  const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
6285
6566
  const runPackage = workspaceCreateNodeRunPackage(runTmpRoot, nodeId, {
6286
- scopedRoot,
6567
+ scopedRoot: runtimeStorageRoot,
6568
+ sourceRoot: scopedRoot,
6287
6569
  cwd,
6288
6570
  task: String(instance.script || instance.scriptRef || instance.body || "").trim(),
6289
6571
  inputValues,
@@ -6357,7 +6639,8 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
6357
6639
  );
6358
6640
  const resultOutputSpec = workspaceResultOutputSpec(graph, nodeId);
6359
6641
  const runPackage = workspaceCreateNodeRunPackage(runTmpRoot, nodeId, {
6360
- scopedRoot,
6642
+ scopedRoot: runtimeStorageRoot,
6643
+ sourceRoot: scopedRoot,
6361
6644
  cwd,
6362
6645
  task: workspaceResolveBodyPlaceholders(instance.body || "", inputValues).trim() || upstreamText,
6363
6646
  inputValues: relevantInputs.values,
@@ -6767,6 +7050,9 @@ export function upsertWorkspaceDeferredRun(meta = {}, deferred = {}) {
6767
7050
  label: String(meta.label || previous.label || "Workspace Run"),
6768
7051
  plannedNodeIds: Array.isArray(meta.plannedNodeIds) ? meta.plannedNodeIds.map(String) : (previous.plannedNodeIds || []),
6769
7052
  workspaceRoot: String(meta.workspaceRoot || previous.workspaceRoot || ""),
7053
+ executionRoot: String(meta.executionRoot || previous.executionRoot || ""),
7054
+ releaseId: String(meta.releaseId || previous.releaseId || ""),
7055
+ designRevision: String(meta.designRevision || previous.designRevision || ""),
6770
7056
  marketplaceResources: Array.isArray(meta.marketplaceResources)
6771
7057
  ? meta.marketplaceResources.map((item) => ({
6772
7058
  kind: String(item?.kind || ""),
@@ -6969,7 +7255,8 @@ export function listWorkspaceScheduleStatuses(root, userCtx = {}) {
6969
7255
  if (scoped.error || !scoped.root) continue;
6970
7256
  let graph;
6971
7257
  try {
6972
- graph = readWorkspaceGraph(scoped.root, root).graph;
7258
+ graph = readWorkspaceStableRelease(scoped.root, root)?.graph
7259
+ || readWorkspaceGraph(scoped.root, root).graph;
6973
7260
  } catch {
6974
7261
  continue;
6975
7262
  }
@@ -6999,6 +7286,9 @@ export function listWorkspaceScheduleStatuses(root, userCtx = {}) {
6999
7286
  rows.push({
7000
7287
  kind: "workspace",
7001
7288
  key,
7289
+ registered: Boolean(registry.schedules?.[key]),
7290
+ ownerUserId: scheduleUserId,
7291
+ ownerUsername: String(current.username || scheduleUserId),
7002
7292
  flowId,
7003
7293
  flowSource,
7004
7294
  workspaceId: String(flow.collaboration?.id || scoped.workspaceId || ""),
@@ -7062,11 +7352,12 @@ export function syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, us
7062
7352
  writeWorkspaceScheduleRegistry({ version: 1, schedules });
7063
7353
  return [];
7064
7354
  }
7065
- const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
7355
+ const effectiveGraph = readWorkspaceStableRelease(scoped?.root || "", root)?.graph || graph;
7356
+ const instances = effectiveGraph?.instances && typeof effectiveGraph.instances === "object" ? effectiveGraph.instances : {};
7066
7357
  for (const [scheduleNodeId, instance] of Object.entries(instances)) {
7067
7358
  if (String(instance?.definitionId || "") !== "workspace_scheduled_run") continue;
7068
7359
  const config = normalizeWorkspaceScheduledRunConfig(instance.body || "");
7069
- const targetRunNodeId = workspaceScheduleInferTargetRunNodeId(graph, scheduleNodeId, config);
7360
+ const targetRunNodeId = workspaceScheduleInferTargetRunNodeId(effectiveGraph, scheduleNodeId, config);
7070
7361
  const key = workspaceScheduleKey(userId, flowSource, flowId, scheduleNodeId);
7071
7362
  const previous = registry.schedules?.[key] && typeof registry.schedules[key] === "object" ? registry.schedules[key] : {};
7072
7363
  const previousNext = Number(previous.nextRunAt || 0);
@@ -7187,7 +7478,15 @@ export async function runWorkspaceScheduledEntry(root, entry) {
7187
7478
  });
7188
7479
  return;
7189
7480
  }
7190
- const graph = hydrateWorkspaceGraphForRuntime(root, scoped, readWorkspaceGraph(scoped.root, root).graph, userCtx);
7481
+ const stableRelease = readWorkspaceStableRelease(scoped.root, root);
7482
+ const executionRoot = stableRelease?.root || scoped.root;
7483
+ const executionScoped = stableRelease ? { ...scoped, root: executionRoot } : scoped;
7484
+ const graph = hydrateWorkspaceGraphForRuntime(
7485
+ root,
7486
+ executionScoped,
7487
+ stableRelease?.graph || readWorkspaceGraph(scoped.root, root).graph,
7488
+ userCtx,
7489
+ );
7191
7490
  const scheduleNodeId = String(entry.scheduleNodeId || entry.key?.split(":").pop() || "");
7192
7491
  const instance = graph.instances?.[scheduleNodeId];
7193
7492
  const config = normalizeWorkspaceScheduledRunConfig(instance?.body || "");
@@ -7221,7 +7520,7 @@ export async function runWorkspaceScheduledEntry(root, entry) {
7221
7520
  appendWorkspaceRunLogEvent(runLog.runId, { type: "scheduler-triggered", scheduleNodeId, runNodeId: targetRunNodeId, cron: config.cron, timezone: config.timezone });
7222
7521
  let plan;
7223
7522
  try {
7224
- plan = workspaceRunPlan(graph, targetRunNodeId, scoped.root);
7523
+ plan = workspaceRunPlan(graph, targetRunNodeId, executionRoot);
7225
7524
  } catch (e) {
7226
7525
  const error = (e && e.message) || String(e);
7227
7526
  appendWorkspaceRunLogEvent(runLog.runId, { type: "error", error });
@@ -7274,8 +7573,17 @@ export async function runWorkspaceScheduledEntry(root, entry) {
7274
7573
  startedAt: Date.now(),
7275
7574
  scheduled: true,
7276
7575
  workspaceRoot: root,
7277
- marketplaceResources: marketplaceResourcesForRun(scoped.root, graph, plannedNodeIds),
7576
+ executionRoot,
7577
+ releaseId: stableRelease?.release?.id || "",
7578
+ designRevision: stableRelease?.release?.designRevision || workspaceDesignRevision(graph),
7579
+ marketplaceResources: marketplaceResourcesForRun(executionRoot, graph, plannedNodeIds),
7278
7580
  };
7581
+ appendWorkspaceRunLogEvent(runLog.runId, {
7582
+ type: "release-resolved",
7583
+ releaseId: runEntry.releaseId || "legacy-current",
7584
+ revision: runEntry.designRevision,
7585
+ source: stableRelease ? "stable" : "legacy-current",
7586
+ });
7279
7587
  activeWorkspaceRuns.set(runKey, runEntry);
7280
7588
  appendWorkspaceRunStarted(runEntry);
7281
7589
  updateWorkspaceScheduleEntry(entry.key, {
@@ -7292,12 +7600,13 @@ export async function runWorkspaceScheduledEntry(root, entry) {
7292
7600
  runControl.setChild(child, childOptions);
7293
7601
  };
7294
7602
  try {
7295
- const result = await runWorkspaceGraph(root, scoped.root, {
7603
+ const result = await runWorkspaceGraph(root, executionRoot, {
7296
7604
  flowId: entry.flowId,
7297
7605
  flowSource: entry.flowSource || "user",
7298
7606
  runNodeId: targetRunNodeId,
7299
7607
  graph,
7300
7608
  }, userCtx, {
7609
+ runtimeRoot: scoped.root,
7301
7610
  signal: controller.signal,
7302
7611
  onActiveChild: setActiveChild,
7303
7612
  onEvent: (event) => appendWorkspaceRunLogEvent(runLog.runId, event),
@@ -7305,7 +7614,12 @@ export async function runWorkspaceScheduledEntry(root, entry) {
7305
7614
  });
7306
7615
  const currentGraph = readWorkspaceGraph(scoped.root, root).graph;
7307
7616
  const touchedIds = workspaceRunTouchedNodeIds(result);
7308
- const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
7617
+ if (stableRelease?.release?.id) {
7618
+ writeWorkspaceReleaseRuntimeState(scoped.root, stableRelease.release.id, result.graph);
7619
+ }
7620
+ const mergedGraph = stableRelease
7621
+ ? mergeWorkspaceRunState(currentGraph, result.graph, touchedIds)
7622
+ : mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
7309
7623
  writeWorkspaceGraph(scoped.root, mergedGraph, root);
7310
7624
  if (result.deferred) {
7311
7625
  const waiting = upsertWorkspaceDeferredRun({
@@ -7333,12 +7647,14 @@ export async function runWorkspaceScheduledEntry(root, entry) {
7333
7647
  ...runEntry,
7334
7648
  endedAt,
7335
7649
  durationMs: endedAt - runEntry.startedAt,
7336
- marketplaceResources: marketplaceResourcesForRun(scoped.root, graph, result.order || Array.from(touchedIds)),
7650
+ marketplaceResources: marketplaceResourcesForRun(executionRoot, graph, result.order || Array.from(touchedIds)),
7337
7651
  }, "success");
7338
7652
  finishWorkspaceRunLogSession(runLog.runId, "success", {
7339
7653
  endedAt,
7340
7654
  durationMs: endedAt - runEntry.startedAt,
7341
7655
  runNodeId: targetRunNodeId,
7656
+ releaseId: runEntry.releaseId,
7657
+ designRevision: runEntry.designRevision,
7342
7658
  });
7343
7659
  updateWorkspaceScheduleEntry(entry.key, {
7344
7660
  nextRunAt: computeNext(config),
@@ -7359,6 +7675,8 @@ export async function runWorkspaceScheduledEntry(root, entry) {
7359
7675
  endedAt,
7360
7676
  durationMs: endedAt - runEntry.startedAt,
7361
7677
  runNodeId: targetRunNodeId,
7678
+ releaseId: runEntry.releaseId,
7679
+ designRevision: runEntry.designRevision,
7362
7680
  error: stopped ? "" : error,
7363
7681
  });
7364
7682
  updateWorkspaceScheduleEntry(entry.key, {
@@ -7386,6 +7704,8 @@ function finishWorkspaceDeferredRun(entry, status, patch = {}) {
7386
7704
  endedAt,
7387
7705
  durationMs: Math.max(0, endedAt - Number(entry.startedAt || endedAt)),
7388
7706
  runNodeId: entry.runNodeId || "",
7707
+ releaseId: entry.releaseId || "",
7708
+ designRevision: entry.designRevision || "",
7389
7709
  error: String(patch.error || ""),
7390
7710
  });
7391
7711
  if (entry.scheduleKey) {
@@ -7431,13 +7751,16 @@ async function runWorkspaceDeferredEntry(root, claimed) {
7431
7751
  if (activeWorkspaceRuns.get(runKey) === runEntry) activeWorkspaceRuns.delete(runKey);
7432
7752
  };
7433
7753
  try {
7434
- const graph = hydrateWorkspaceGraphForRuntime(root, scoped, readWorkspaceGraph(scoped.root, root).graph, userCtx);
7435
- const result = await runWorkspaceGraph(root, scoped.root, {
7754
+ const executionRoot = String(claimed.executionRoot || "").trim() || scoped.root;
7755
+ const executionScoped = executionRoot === scoped.root ? scoped : { ...scoped, root: executionRoot };
7756
+ const graph = hydrateWorkspaceGraphForRuntime(root, executionScoped, readWorkspaceGraph(executionRoot, root).graph, userCtx);
7757
+ const result = await runWorkspaceGraph(root, executionRoot, {
7436
7758
  flowId: claimed.flowId,
7437
7759
  flowSource: claimed.flowSource || "user",
7438
7760
  runNodeId: claimed.runNodeId,
7439
7761
  graph,
7440
7762
  }, userCtx, {
7763
+ runtimeRoot: scoped.root,
7441
7764
  signal: controller.signal,
7442
7765
  onActiveChild: (child, options = {}) => runControl.setChild(child, options),
7443
7766
  onEvent: (event) => appendWorkspaceRunLogEvent(claimed.runId, event),
@@ -7445,7 +7768,12 @@ async function runWorkspaceDeferredEntry(root, claimed) {
7445
7768
  });
7446
7769
  const currentGraph = readWorkspaceGraph(scoped.root, root).graph;
7447
7770
  const touchedIds = workspaceRunTouchedNodeIds(result);
7448
- const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
7771
+ if (claimed.releaseId) {
7772
+ writeWorkspaceReleaseRuntimeState(scoped.root, claimed.releaseId, result.graph);
7773
+ }
7774
+ const mergedGraph = claimed.releaseId
7775
+ ? mergeWorkspaceRunState(currentGraph, result.graph, touchedIds)
7776
+ : mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
7449
7777
  writeWorkspaceGraph(scoped.root, mergedGraph, root);
7450
7778
  if (result.deferred) {
7451
7779
  const waiting = upsertWorkspaceDeferredRun({ ...claimed, deferredKey: claimed.key }, result.deferred);