@fieldwangai/agentflow 0.1.163 → 0.1.164

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 || {});
@@ -4668,12 +4930,13 @@ function workspaceCreateNodeTmpDir(runTmpRoot, nodeId) {
4668
4930
  return dir;
4669
4931
  }
4670
4932
 
4671
- function workspaceCreateNodeRunPackage(runTmpRoot, nodeId, { scopedRoot, cwd = "", task = "", inputValues = {}, skillsBlock = "", mcpBlock = "", resultFile = "", outParamFiles = {}, durableOutputs = false } = {}) {
4933
+ function workspaceCreateNodeRunPackage(runTmpRoot, nodeId, { scopedRoot, sourceRoot = "", cwd = "", task = "", inputValues = {}, skillsBlock = "", mcpBlock = "", resultFile = "", outParamFiles = {}, durableOutputs = false } = {}) {
4672
4934
  const nodeRunDir = workspaceCreateNodeTmpDir(runTmpRoot, nodeId);
4673
4935
  const nodeTmpDir = path.join(nodeRunDir, "tmp");
4674
4936
  const legacyOutputsDir = path.join(nodeRunDir, "outputs");
4675
- const workspaceRoot = path.resolve(scopedRoot);
4676
- const workspaceOutputsDir = path.join(workspaceRoot, "outputs");
4937
+ const workspaceRoot = path.resolve(sourceRoot || scopedRoot);
4938
+ const outputWorkspaceRoot = path.resolve(scopedRoot);
4939
+ const workspaceOutputsDir = path.join(outputWorkspaceRoot, "outputs");
4677
4940
  const nodePart = workspaceSanitizeTmpSegment(nodeId || "node", "node");
4678
4941
  const outputsRel = durableOutputs ? path.posix.join("outputs", nodePart) : "outputs";
4679
4942
  const outputsDir = durableOutputs ? path.join(workspaceOutputsDir, nodePart) : legacyOutputsDir;
@@ -4703,6 +4966,7 @@ function workspaceCreateNodeRunPackage(runTmpRoot, nodeId, { scopedRoot, cwd = "
4703
4966
  outputsRel,
4704
4967
  directWorkspaceOutputs: durableOutputs,
4705
4968
  workspaceRoot,
4969
+ outputWorkspaceRoot,
4706
4970
  workspaceOutputsDir,
4707
4971
  executionCwd: cwd ? path.resolve(cwd) : workspaceRoot,
4708
4972
  resultFileRel,
@@ -5277,22 +5541,23 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
5277
5541
  emit({ type: "status", nodeId, line: `Timing ${label}: ${elapsedMs}ms`, timing: { label, elapsedMs, ...extra } });
5278
5542
  };
5279
5543
  let cwd = scopedRoot;
5544
+ const runtimeStorageRoot = path.resolve(opts.runtimeRoot || scopedRoot);
5280
5545
  const modelKey = typeof payload?.model === "string" ? payload.model.trim() : "";
5281
5546
  const runEnv = {};
5282
5547
  const runtimeEnv = (extra = {}) => runtimeEnvForUser(userCtx, { ...runEnv, ...(extra || {}) });
5283
5548
  const autoCleanupWorktrees = [];
5284
- const runTmpRoot = workspaceCreateRunTmpRoot(scopedRoot, runNodeId);
5549
+ const runTmpRoot = workspaceCreateRunTmpRoot(runtimeStorageRoot, runNodeId);
5285
5550
  const runtimeRunId = String(opts.runId || payload?.runId || "").trim() || runLedgerId("workspace-execution");
5286
5551
  const ownsRunManifest = !(Array.isArray(opts?.subflowCallStack) && opts.subflowCallStack.length);
5287
5552
  const persistRunManifest = (status, extra = {}) => {
5288
5553
  if (!ownsRunManifest) return null;
5289
- return workspaceWriteRunManifest(scopedRoot, runtimeRunId, {
5554
+ return workspaceWriteRunManifest(runtimeStorageRoot, runtimeRunId, {
5290
5555
  flowId: String(payload?.flowId || ""),
5291
5556
  flowSource: String(payload?.flowSource || "user"),
5292
5557
  runNodeId,
5293
5558
  status,
5294
5559
  runtimeRoot: runTmpRoot,
5295
- artifactRoot: path.join(path.resolve(scopedRoot), "outputs"),
5560
+ artifactRoot: path.join(runtimeStorageRoot, "outputs"),
5296
5561
  worktrees: autoCleanupWorktrees.map((entry) => ({ ...entry, removed: false })),
5297
5562
  ...extra,
5298
5563
  });
@@ -5705,7 +5970,8 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
5705
5970
  });
5706
5971
  }
5707
5972
  const runPackage = workspaceCreateNodeRunPackage(runTmpRoot, `${nodeId}-iteration-${iteration}`, {
5708
- scopedRoot,
5973
+ scopedRoot: runtimeStorageRoot,
5974
+ sourceRoot: scopedRoot,
5709
5975
  cwd,
5710
5976
  task: stepScript || stepScriptRef,
5711
5977
  inputValues: iterationInputs,
@@ -6283,7 +6549,8 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
6283
6549
  const prepareStartedAt = Date.now();
6284
6550
  const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
6285
6551
  const runPackage = workspaceCreateNodeRunPackage(runTmpRoot, nodeId, {
6286
- scopedRoot,
6552
+ scopedRoot: runtimeStorageRoot,
6553
+ sourceRoot: scopedRoot,
6287
6554
  cwd,
6288
6555
  task: String(instance.script || instance.scriptRef || instance.body || "").trim(),
6289
6556
  inputValues,
@@ -6357,7 +6624,8 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
6357
6624
  );
6358
6625
  const resultOutputSpec = workspaceResultOutputSpec(graph, nodeId);
6359
6626
  const runPackage = workspaceCreateNodeRunPackage(runTmpRoot, nodeId, {
6360
- scopedRoot,
6627
+ scopedRoot: runtimeStorageRoot,
6628
+ sourceRoot: scopedRoot,
6361
6629
  cwd,
6362
6630
  task: workspaceResolveBodyPlaceholders(instance.body || "", inputValues).trim() || upstreamText,
6363
6631
  inputValues: relevantInputs.values,
@@ -6767,6 +7035,9 @@ export function upsertWorkspaceDeferredRun(meta = {}, deferred = {}) {
6767
7035
  label: String(meta.label || previous.label || "Workspace Run"),
6768
7036
  plannedNodeIds: Array.isArray(meta.plannedNodeIds) ? meta.plannedNodeIds.map(String) : (previous.plannedNodeIds || []),
6769
7037
  workspaceRoot: String(meta.workspaceRoot || previous.workspaceRoot || ""),
7038
+ executionRoot: String(meta.executionRoot || previous.executionRoot || ""),
7039
+ releaseId: String(meta.releaseId || previous.releaseId || ""),
7040
+ designRevision: String(meta.designRevision || previous.designRevision || ""),
6770
7041
  marketplaceResources: Array.isArray(meta.marketplaceResources)
6771
7042
  ? meta.marketplaceResources.map((item) => ({
6772
7043
  kind: String(item?.kind || ""),
@@ -6969,7 +7240,8 @@ export function listWorkspaceScheduleStatuses(root, userCtx = {}) {
6969
7240
  if (scoped.error || !scoped.root) continue;
6970
7241
  let graph;
6971
7242
  try {
6972
- graph = readWorkspaceGraph(scoped.root, root).graph;
7243
+ graph = readWorkspaceStableRelease(scoped.root, root)?.graph
7244
+ || readWorkspaceGraph(scoped.root, root).graph;
6973
7245
  } catch {
6974
7246
  continue;
6975
7247
  }
@@ -6999,6 +7271,9 @@ export function listWorkspaceScheduleStatuses(root, userCtx = {}) {
6999
7271
  rows.push({
7000
7272
  kind: "workspace",
7001
7273
  key,
7274
+ registered: Boolean(registry.schedules?.[key]),
7275
+ ownerUserId: scheduleUserId,
7276
+ ownerUsername: String(current.username || scheduleUserId),
7002
7277
  flowId,
7003
7278
  flowSource,
7004
7279
  workspaceId: String(flow.collaboration?.id || scoped.workspaceId || ""),
@@ -7062,11 +7337,12 @@ export function syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, us
7062
7337
  writeWorkspaceScheduleRegistry({ version: 1, schedules });
7063
7338
  return [];
7064
7339
  }
7065
- const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
7340
+ const effectiveGraph = readWorkspaceStableRelease(scoped?.root || "", root)?.graph || graph;
7341
+ const instances = effectiveGraph?.instances && typeof effectiveGraph.instances === "object" ? effectiveGraph.instances : {};
7066
7342
  for (const [scheduleNodeId, instance] of Object.entries(instances)) {
7067
7343
  if (String(instance?.definitionId || "") !== "workspace_scheduled_run") continue;
7068
7344
  const config = normalizeWorkspaceScheduledRunConfig(instance.body || "");
7069
- const targetRunNodeId = workspaceScheduleInferTargetRunNodeId(graph, scheduleNodeId, config);
7345
+ const targetRunNodeId = workspaceScheduleInferTargetRunNodeId(effectiveGraph, scheduleNodeId, config);
7070
7346
  const key = workspaceScheduleKey(userId, flowSource, flowId, scheduleNodeId);
7071
7347
  const previous = registry.schedules?.[key] && typeof registry.schedules[key] === "object" ? registry.schedules[key] : {};
7072
7348
  const previousNext = Number(previous.nextRunAt || 0);
@@ -7187,7 +7463,15 @@ export async function runWorkspaceScheduledEntry(root, entry) {
7187
7463
  });
7188
7464
  return;
7189
7465
  }
7190
- const graph = hydrateWorkspaceGraphForRuntime(root, scoped, readWorkspaceGraph(scoped.root, root).graph, userCtx);
7466
+ const stableRelease = readWorkspaceStableRelease(scoped.root, root);
7467
+ const executionRoot = stableRelease?.root || scoped.root;
7468
+ const executionScoped = stableRelease ? { ...scoped, root: executionRoot } : scoped;
7469
+ const graph = hydrateWorkspaceGraphForRuntime(
7470
+ root,
7471
+ executionScoped,
7472
+ stableRelease?.graph || readWorkspaceGraph(scoped.root, root).graph,
7473
+ userCtx,
7474
+ );
7191
7475
  const scheduleNodeId = String(entry.scheduleNodeId || entry.key?.split(":").pop() || "");
7192
7476
  const instance = graph.instances?.[scheduleNodeId];
7193
7477
  const config = normalizeWorkspaceScheduledRunConfig(instance?.body || "");
@@ -7221,7 +7505,7 @@ export async function runWorkspaceScheduledEntry(root, entry) {
7221
7505
  appendWorkspaceRunLogEvent(runLog.runId, { type: "scheduler-triggered", scheduleNodeId, runNodeId: targetRunNodeId, cron: config.cron, timezone: config.timezone });
7222
7506
  let plan;
7223
7507
  try {
7224
- plan = workspaceRunPlan(graph, targetRunNodeId, scoped.root);
7508
+ plan = workspaceRunPlan(graph, targetRunNodeId, executionRoot);
7225
7509
  } catch (e) {
7226
7510
  const error = (e && e.message) || String(e);
7227
7511
  appendWorkspaceRunLogEvent(runLog.runId, { type: "error", error });
@@ -7274,8 +7558,17 @@ export async function runWorkspaceScheduledEntry(root, entry) {
7274
7558
  startedAt: Date.now(),
7275
7559
  scheduled: true,
7276
7560
  workspaceRoot: root,
7277
- marketplaceResources: marketplaceResourcesForRun(scoped.root, graph, plannedNodeIds),
7561
+ executionRoot,
7562
+ releaseId: stableRelease?.release?.id || "",
7563
+ designRevision: stableRelease?.release?.designRevision || workspaceDesignRevision(graph),
7564
+ marketplaceResources: marketplaceResourcesForRun(executionRoot, graph, plannedNodeIds),
7278
7565
  };
7566
+ appendWorkspaceRunLogEvent(runLog.runId, {
7567
+ type: "release-resolved",
7568
+ releaseId: runEntry.releaseId || "legacy-current",
7569
+ revision: runEntry.designRevision,
7570
+ source: stableRelease ? "stable" : "legacy-current",
7571
+ });
7279
7572
  activeWorkspaceRuns.set(runKey, runEntry);
7280
7573
  appendWorkspaceRunStarted(runEntry);
7281
7574
  updateWorkspaceScheduleEntry(entry.key, {
@@ -7292,12 +7585,13 @@ export async function runWorkspaceScheduledEntry(root, entry) {
7292
7585
  runControl.setChild(child, childOptions);
7293
7586
  };
7294
7587
  try {
7295
- const result = await runWorkspaceGraph(root, scoped.root, {
7588
+ const result = await runWorkspaceGraph(root, executionRoot, {
7296
7589
  flowId: entry.flowId,
7297
7590
  flowSource: entry.flowSource || "user",
7298
7591
  runNodeId: targetRunNodeId,
7299
7592
  graph,
7300
7593
  }, userCtx, {
7594
+ runtimeRoot: scoped.root,
7301
7595
  signal: controller.signal,
7302
7596
  onActiveChild: setActiveChild,
7303
7597
  onEvent: (event) => appendWorkspaceRunLogEvent(runLog.runId, event),
@@ -7305,7 +7599,12 @@ export async function runWorkspaceScheduledEntry(root, entry) {
7305
7599
  });
7306
7600
  const currentGraph = readWorkspaceGraph(scoped.root, root).graph;
7307
7601
  const touchedIds = workspaceRunTouchedNodeIds(result);
7308
- const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
7602
+ if (stableRelease?.release?.id) {
7603
+ writeWorkspaceReleaseRuntimeState(scoped.root, stableRelease.release.id, result.graph);
7604
+ }
7605
+ const mergedGraph = stableRelease
7606
+ ? mergeWorkspaceRunState(currentGraph, result.graph, touchedIds)
7607
+ : mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
7309
7608
  writeWorkspaceGraph(scoped.root, mergedGraph, root);
7310
7609
  if (result.deferred) {
7311
7610
  const waiting = upsertWorkspaceDeferredRun({
@@ -7333,12 +7632,14 @@ export async function runWorkspaceScheduledEntry(root, entry) {
7333
7632
  ...runEntry,
7334
7633
  endedAt,
7335
7634
  durationMs: endedAt - runEntry.startedAt,
7336
- marketplaceResources: marketplaceResourcesForRun(scoped.root, graph, result.order || Array.from(touchedIds)),
7635
+ marketplaceResources: marketplaceResourcesForRun(executionRoot, graph, result.order || Array.from(touchedIds)),
7337
7636
  }, "success");
7338
7637
  finishWorkspaceRunLogSession(runLog.runId, "success", {
7339
7638
  endedAt,
7340
7639
  durationMs: endedAt - runEntry.startedAt,
7341
7640
  runNodeId: targetRunNodeId,
7641
+ releaseId: runEntry.releaseId,
7642
+ designRevision: runEntry.designRevision,
7342
7643
  });
7343
7644
  updateWorkspaceScheduleEntry(entry.key, {
7344
7645
  nextRunAt: computeNext(config),
@@ -7359,6 +7660,8 @@ export async function runWorkspaceScheduledEntry(root, entry) {
7359
7660
  endedAt,
7360
7661
  durationMs: endedAt - runEntry.startedAt,
7361
7662
  runNodeId: targetRunNodeId,
7663
+ releaseId: runEntry.releaseId,
7664
+ designRevision: runEntry.designRevision,
7362
7665
  error: stopped ? "" : error,
7363
7666
  });
7364
7667
  updateWorkspaceScheduleEntry(entry.key, {
@@ -7386,6 +7689,8 @@ function finishWorkspaceDeferredRun(entry, status, patch = {}) {
7386
7689
  endedAt,
7387
7690
  durationMs: Math.max(0, endedAt - Number(entry.startedAt || endedAt)),
7388
7691
  runNodeId: entry.runNodeId || "",
7692
+ releaseId: entry.releaseId || "",
7693
+ designRevision: entry.designRevision || "",
7389
7694
  error: String(patch.error || ""),
7390
7695
  });
7391
7696
  if (entry.scheduleKey) {
@@ -7431,13 +7736,16 @@ async function runWorkspaceDeferredEntry(root, claimed) {
7431
7736
  if (activeWorkspaceRuns.get(runKey) === runEntry) activeWorkspaceRuns.delete(runKey);
7432
7737
  };
7433
7738
  try {
7434
- const graph = hydrateWorkspaceGraphForRuntime(root, scoped, readWorkspaceGraph(scoped.root, root).graph, userCtx);
7435
- const result = await runWorkspaceGraph(root, scoped.root, {
7739
+ const executionRoot = String(claimed.executionRoot || "").trim() || scoped.root;
7740
+ const executionScoped = executionRoot === scoped.root ? scoped : { ...scoped, root: executionRoot };
7741
+ const graph = hydrateWorkspaceGraphForRuntime(root, executionScoped, readWorkspaceGraph(executionRoot, root).graph, userCtx);
7742
+ const result = await runWorkspaceGraph(root, executionRoot, {
7436
7743
  flowId: claimed.flowId,
7437
7744
  flowSource: claimed.flowSource || "user",
7438
7745
  runNodeId: claimed.runNodeId,
7439
7746
  graph,
7440
7747
  }, userCtx, {
7748
+ runtimeRoot: scoped.root,
7441
7749
  signal: controller.signal,
7442
7750
  onActiveChild: (child, options = {}) => runControl.setChild(child, options),
7443
7751
  onEvent: (event) => appendWorkspaceRunLogEvent(claimed.runId, event),
@@ -7445,7 +7753,12 @@ async function runWorkspaceDeferredEntry(root, claimed) {
7445
7753
  });
7446
7754
  const currentGraph = readWorkspaceGraph(scoped.root, root).graph;
7447
7755
  const touchedIds = workspaceRunTouchedNodeIds(result);
7448
- const mergedGraph = mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
7756
+ if (claimed.releaseId) {
7757
+ writeWorkspaceReleaseRuntimeState(scoped.root, claimed.releaseId, result.graph);
7758
+ }
7759
+ const mergedGraph = claimed.releaseId
7760
+ ? mergeWorkspaceRunState(currentGraph, result.graph, touchedIds)
7761
+ : mergeWorkspaceRunGraph(currentGraph, result.graph, touchedIds);
7449
7762
  writeWorkspaceGraph(scoped.root, mergedGraph, root);
7450
7763
  if (result.deferred) {
7451
7764
  const waiting = upsertWorkspaceDeferredRun({ ...claimed, deferredKey: claimed.key }, result.deferred);