@fieldwangai/agentflow 0.1.162 → 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.
Files changed (42) hide show
  1. package/bin/lib/flow-dsl/codegen.mjs +23 -2
  2. package/bin/lib/flow-dsl/lint.mjs +44 -0
  3. package/bin/lib/flow-dsl/parser.mjs +68 -1
  4. package/bin/lib/marketplace-usage.mjs +218 -0
  5. package/bin/lib/marketplace.mjs +183 -3
  6. package/bin/lib/node-package-manifest.mjs +1 -1
  7. package/bin/lib/spaces.mjs +200 -0
  8. package/bin/lib/ui-server.mjs +314 -76
  9. package/bin/lib/workspace-routes.mjs +396 -5
  10. package/bin/lib/workspace-run-logs.mjs +2 -0
  11. package/bin/lib/workspace-server.mjs +698 -48
  12. package/bin/lib/workspace-state.mjs +2 -1
  13. package/builtin/nodes/agent_subAgent.md +5 -1
  14. package/builtin/nodes/context_bundle.md +44 -0
  15. package/builtin/nodes/context_knowledge.md +29 -0
  16. package/builtin/nodes/context_skills.md +29 -0
  17. package/builtin/nodes/context_workspace.md +36 -0
  18. package/builtin/nodes/control_while.md +7 -0
  19. package/builtin/nodes/tool_git_worktree_load.md +4 -3
  20. package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-CTrXOZ00.js → WorkflowAssistantThread-CffIx5BY.js} +1 -1
  21. package/builtin/web-ui/dist/assets/index-CDItaRfX.css +1 -0
  22. package/builtin/web-ui/dist/assets/index-CWIcfWHO.js +873 -0
  23. package/builtin/web-ui/dist/index.html +2 -2
  24. package/package.json +1 -1
  25. package/shared/slot-types.js +1 -0
  26. package/skills/agentflow-cli/SKILL.md +50 -3
  27. package/skills/agentflow-cli/runtime/bin/lib/skill-runtime.mjs +120 -6
  28. package/skills/agentflow-cli/runtime/builtin/nodes/agent_subAgent.md +5 -1
  29. package/skills/agentflow-cli/runtime/builtin/nodes/context_bundle.md +44 -0
  30. package/skills/agentflow-cli/runtime/builtin/nodes/context_knowledge.md +29 -0
  31. package/skills/agentflow-cli/runtime/builtin/nodes/context_skills.md +29 -0
  32. package/skills/agentflow-cli/runtime/builtin/nodes/context_workspace.md +36 -0
  33. package/skills/agentflow-cli/runtime/builtin/nodes/control_while.md +7 -0
  34. package/skills/agentflow-cli/runtime/builtin/nodes/tool_git_worktree_load.md +4 -3
  35. package/skills/agentflow-cli/runtime/package.json +1 -1
  36. package/skills/agentflow-cli/scripts/agentflow-cli.mjs +64 -0
  37. package/skills/agentflow-flow-dsl/SKILL.md +48 -2
  38. package/skills/agentflow-flow-dsl/references/node-calls.md +6 -2
  39. package/skills/agentflow-flow-dsl/references/subflow-authoring.md +34 -7
  40. package/skills/agentflow-node-reference/references/builtin-nodes.md +37 -5
  41. package/builtin/web-ui/dist/assets/index-5uJFccdX.css +0 -1
  42. package/builtin/web-ui/dist/assets/index-BeUfNQRL.js +0 -873
@@ -33,6 +33,7 @@ import { t } from "./i18n.mjs";
33
33
  import { advanceJenkinsBuild, createJenkinsHttpInvoker, jenkinsBuildStatePath, normalizeJenkinsBuildConfig, readJenkinsBuildState, writeJenkinsBuildState } from "./jenkins.mjs";
34
34
  import { log } from "./log.mjs";
35
35
  import { resolveMarketplaceNodePackage } from "./marketplace.mjs";
36
+ import { marketplaceResourcesForRun, recordMarketplaceRunUsage } from "./marketplace-usage.mjs";
36
37
  import { PACKAGE_ROOT, getAgentflowDataRoot, getAgentflowUserDataRoot, listAgentflowUserIds } from "./paths.mjs";
37
38
  import { appendRunLedgerEvent, readRunLedgerEvents, runLedgerId } from "./run-ledger.mjs";
38
39
  import { computeNextRunAt } from "./schedule-config.mjs";
@@ -41,9 +42,10 @@ import { readMergedEnvObject, runtimeEnvForUser } from "./user-env.mjs";
41
42
  import { sendWecomAppMarkdown, sendWecomGroupMarkdown } from "./wecom.mjs";
42
43
  import { getWorkspaceCollaborationByFlow, getWorkspaceCollaborationForProject, listWorkspaceCollaborationsForUser, workspaceCollaborationAccess, workspaceCollaborationSummary } from "./workspace-collaboration.mjs";
43
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";
44
46
  import { createWorkspaceRunController, terminateWorkspaceChild } from "./workspace-run-controller.mjs";
45
47
  import { appendWorkspaceRunLogEvent, createWorkspaceRunLogSession, finishWorkspaceRunLogSession } from "./workspace-run-logs.mjs";
46
- import { splitWorkspaceGraph } from "./workspace-state.mjs";
48
+ import { mergeWorkspaceState, splitWorkspaceGraph } from "./workspace-state.mjs";
47
49
  import { isWorkspaceDraftDir } from "./workspace-draft.mjs";
48
50
  import { getPipelineFiles } from "./workspace-tree.mjs";
49
51
  import { spawn } from "child_process";
@@ -707,6 +709,249 @@ export function readWorkspaceGraph(workspaceRoot, marketplaceRoot = "") {
707
709
  return { path: designPath, graph };
708
710
  }
709
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
+
710
955
  const DISPLAY_SHARE_FILENAME = "display-shares.json";
711
956
 
712
957
  const DISPLAY_SHARE_ALLOWED_EXPIRY_DAYS = new Set([1, 7, 30, 90, 365]);
@@ -796,6 +1041,71 @@ export function readWorkspacesFromPath(p, userCtx = {}) {
796
1041
  }
797
1042
  }
798
1043
 
1044
+ function legacyUserWorkspacesPath(userCtx = {}) {
1045
+ return path.join(getAgentflowUserDataRoot(userCtx.userId || ""), USER_WORKSPACES_FILENAME);
1046
+ }
1047
+
1048
+ function readLegacyAdminWorkspaces(userCtx = {}) {
1049
+ const users = readAuthUsers();
1050
+ const candidates = [];
1051
+ for (const [userId, user] of Object.entries(users || {})) {
1052
+ if (user?.isAdmin) candidates.push(String(userId || ""));
1053
+ }
1054
+ if (userCtx?.isAdmin && userCtx.userId) candidates.unshift(String(userCtx.userId));
1055
+ const seenPaths = new Set();
1056
+ const seenEntries = new Set();
1057
+ const out = [];
1058
+ for (const userId of candidates) {
1059
+ const p = legacyUserWorkspacesPath({ userId });
1060
+ const resolved = path.resolve(p);
1061
+ if (seenPaths.has(resolved) || resolved === path.resolve(workspacesPath())) continue;
1062
+ seenPaths.add(resolved);
1063
+ for (const entry of readWorkspacesFromPath(p, { userId })) {
1064
+ const key = entry.id || entry.path || entry.repoUrl;
1065
+ if (seenEntries.has(key)) continue;
1066
+ seenEntries.add(key);
1067
+ out.push(entry);
1068
+ }
1069
+ }
1070
+ return out;
1071
+ }
1072
+
1073
+ /** The same authenticated Workspace catalog used by GET /api/workspaces. */
1074
+ export function readUserWorkspaces(userCtx = {}) {
1075
+ const globalPath = workspacesPath();
1076
+ const globalWorkspaces = fs.existsSync(globalPath) ? readWorkspacesFromPath(globalPath, userCtx) : [];
1077
+ const adminLegacy = readLegacyAdminWorkspaces(userCtx);
1078
+ if (globalWorkspaces.length || adminLegacy.length) {
1079
+ const seen = new Set();
1080
+ const out = [];
1081
+ for (const entry of [...globalWorkspaces, ...adminLegacy]) {
1082
+ const key = entry.id || entry.path || entry.repoUrl;
1083
+ if (seen.has(key)) continue;
1084
+ seen.add(key);
1085
+ out.push(entry);
1086
+ }
1087
+ return out;
1088
+ }
1089
+ return readWorkspacesFromPath(legacyUserWorkspacesPath(userCtx), userCtx);
1090
+ }
1091
+
1092
+ export function listConfiguredWorkspaces(root, scopedRoot, userCtx = {}) {
1093
+ const currentRoot = path.resolve(scopedRoot || root);
1094
+ const homeRoot = path.resolve(os.homedir());
1095
+ const builtins = [
1096
+ { id: "current", label: "当前流程工作区", kind: "local", path: currentRoot, builtin: true, exists: fs.existsSync(currentRoot) && fs.statSync(currentRoot).isDirectory(), type: "flow", enabled: true },
1097
+ { id: "home", label: "用户 Home", kind: "local", path: homeRoot, builtin: true, exists: fs.existsSync(homeRoot) && fs.statSync(homeRoot).isDirectory(), type: "local", enabled: true },
1098
+ ];
1099
+ const custom = readUserWorkspaces(userCtx).filter((entry) => entry.enabled !== false).map((entry) => ({ ...entry, builtin: false }));
1100
+ const seen = new Set();
1101
+ return [...builtins, ...custom].filter((entry) => {
1102
+ const key = path.resolve(entry.path);
1103
+ if (seen.has(key)) return false;
1104
+ seen.add(key);
1105
+ return true;
1106
+ });
1107
+ }
1108
+
799
1109
  export function workspaceRepoUrlWithCredential(repoUrl = "", credential = "") {
800
1110
  const token = String(credential || "").trim();
801
1111
  if (!token) return String(repoUrl || "").trim();
@@ -856,6 +1166,10 @@ export function appendWorkspaceRunFinished(record, status) {
856
1166
  durationMs: Math.max(0, Number(record.durationMs || (Number(record.endedAt || Date.now()) - Number(record.startedAt || record.at || Date.now())))),
857
1167
  status,
858
1168
  });
1169
+ recordMarketplaceRunUsage(record.workspaceRoot || record.root || "", record.marketplaceResources || [], {
1170
+ ...record,
1171
+ status,
1172
+ });
859
1173
  }
860
1174
 
861
1175
  function normalizeWorkspaceUsageRecord(parsed, source = "workspace-run") {
@@ -1034,7 +1348,7 @@ function normalizeDisplayShareLayout(layout, fallback = "canvas") {
1034
1348
  return ["canvas", "gallery", "slides", "document", "single"].includes(text) ? text : fallback;
1035
1349
  }
1036
1350
 
1037
- export function createDisplayShareRecord({ userId, flowId, flowSource, archived, title, layout, nodeIds, expiresMode, expiresInDays, permanent, expiresAt }) {
1351
+ export function createDisplayShareRecord({ userId, flowId, flowSource, archived, title, layout, nodeIds, expiresMode, expiresInDays, permanent, expiresAt, visibility = "public" }) {
1038
1352
  const shares = readDisplayShares();
1039
1353
  let id = createDisplayShareId();
1040
1354
  while (shares[id]) id = createDisplayShareId();
@@ -1050,6 +1364,7 @@ export function createDisplayShareRecord({ userId, flowId, flowSource, archived,
1050
1364
  title: String(title || "").trim() || "AgentFlow Display",
1051
1365
  layout: normalizeDisplayShareLayout(layout, "canvas"),
1052
1366
  nodeIds: Array.isArray(nodeIds) ? nodeIds : [],
1367
+ visibility: String(visibility || "").trim().toLowerCase() === "private" ? "private" : "public",
1053
1368
  createdAt: now,
1054
1369
  updatedAt: now,
1055
1370
  expiresAt: expiry.expiresAt,
@@ -1181,6 +1496,24 @@ export function mergeWorkspaceRunGraph(currentGraph, runGraph, touchedIds) {
1181
1496
  };
1182
1497
  }
1183
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
+
1184
1517
  export function mergeWorkspacePersistentNodeRefs(incomingGraph, currentGraph) {
1185
1518
  const incoming = normalizeWorkspaceGraphPayload(incomingGraph || {});
1186
1519
  const current = normalizeWorkspaceGraphPayload(currentGraph || {});
@@ -3175,7 +3508,7 @@ function workspaceTargetSlotForEdge(graph, edge) {
3175
3508
  function isWorkspaceSemanticInputSlot(slot) {
3176
3509
  const name = String(slot?.name || "");
3177
3510
  const type = String(slot?.type || "");
3178
- return type === "node" || name === "prev" || name === "next" || name === "skillsContext" || name === "mcpContext" || name === "knowledgeContext" || name === "workspaceContext" || name === "gitContext";
3511
+ return type === "node" || type === "context" || name === "prev" || name === "next" || name === "context" || name === "skillsContext" || name === "mcpContext" || name === "knowledgeContext" || name === "workspaceContext" || name === "gitContext";
3179
3512
  }
3180
3513
 
3181
3514
  function workspaceAgentInputBlock(inputValues = {}, inputMounts = {}) {
@@ -3230,8 +3563,9 @@ function workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputNames =
3230
3563
  return workspaceOutputSlotValueForEdge(graph, outputs, contentEdge, scopedRoot);
3231
3564
  }
3232
3565
 
3233
- function workspaceInputValues(graph, nodeId, outputs, scopedRoot = "") {
3566
+ function workspaceInputValues(graph, nodeId, outputs, scopedRoot = "", options = {}) {
3234
3567
  const values = {};
3568
+ const includeContext = options?.includeContext === true;
3235
3569
  const edges = Array.isArray(graph?.edges) ? graph.edges : [];
3236
3570
  const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
3237
3571
  const target = instances[String(nodeId || "")] || {};
@@ -3241,13 +3575,13 @@ function workspaceInputValues(graph, nodeId, outputs, scopedRoot = "") {
3241
3575
  const index = workspaceHandleIndex(edge?.targetHandle, "input");
3242
3576
  const slot = inputSlots[index] || null;
3243
3577
  const name = String(slot?.name || "").trim();
3244
- if (!name || isWorkspaceSemanticInputSlot(slot)) continue;
3578
+ if (!name || (isWorkspaceSemanticInputSlot(slot) && !(includeContext && name === "context"))) continue;
3245
3579
  const value = workspaceOutputSlotValueForEdge(graph, outputs, edge, scopedRoot);
3246
3580
  if (String(value || "").trim()) values[name] = String(value);
3247
3581
  }
3248
3582
  for (const slot of inputSlots) {
3249
3583
  const name = String(slot?.name || "").trim();
3250
- if (!name || isWorkspaceSemanticInputSlot(slot) || Object.prototype.hasOwnProperty.call(values, name)) continue;
3584
+ if (!name || (isWorkspaceSemanticInputSlot(slot) && !(includeContext && name === "context")) || Object.prototype.hasOwnProperty.call(values, name)) continue;
3251
3585
  const value = workspaceSlotValue(slot);
3252
3586
  if (String(value || "").trim()) values[name] = String(value);
3253
3587
  }
@@ -3944,7 +4278,9 @@ function selectedSkillKeysFromInstance(instance) {
3944
4278
 
3945
4279
  function selectedSkillKeysFromConfigSlots(instance) {
3946
4280
  const slots = [...(Array.isArray(instance?.input) ? instance.input : []), ...(Array.isArray(instance?.output) ? instance.output : [])];
3947
- const slot = slots.find((item) => item?.name === "skillsContext") || slots.find((item) => item?.name === "skillKeys");
4281
+ const slot = slots.find((item) => item?.name === "skills") ||
4282
+ slots.find((item) => item?.name === "skillsContext") ||
4283
+ slots.find((item) => item?.name === "skillKeys");
3948
4284
  return parseWorkspaceSkillKeys(workspaceSlotValue(slot) || "");
3949
4285
  }
3950
4286
 
@@ -3998,6 +4334,25 @@ function workspaceSemanticInputText(graph, nodeId, outputs, name, scopedRoot = "
3998
4334
  return workspaceSlotValue(workspaceSlotByName(instance, targetName));
3999
4335
  }
4000
4336
 
4337
+ function workspaceContextBundleFromText(text) {
4338
+ const parsed = parseJsonText(String(text || "").trim(), null);
4339
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
4340
+ if (Number(parsed.version || 1) !== 1) return {};
4341
+ const out = {};
4342
+ for (const name of ["knowledgeContext", "skillsContext", "workspaceContext", "mcpContext"]) {
4343
+ if (parsed[name] !== undefined && parsed[name] !== null) {
4344
+ out[name] = typeof parsed[name] === "string" ? parsed[name] : JSON.stringify(parsed[name]);
4345
+ }
4346
+ }
4347
+ return out;
4348
+ }
4349
+
4350
+ function workspaceNodeContextBundle(graph, nodeId, outputs, scopedRoot = "") {
4351
+ return workspaceContextBundleFromText(
4352
+ workspaceSemanticInputText(graph, nodeId, outputs, "context", scopedRoot),
4353
+ );
4354
+ }
4355
+
4001
4356
  function workspaceContextObjectFromText(text, baseCwd, scopedRoot) {
4002
4357
  const raw = String(text || "").trim();
4003
4358
  if (!raw) return null;
@@ -4024,20 +4379,30 @@ function workspaceLooksLikeKnowledgePath(value) {
4024
4379
  }
4025
4380
 
4026
4381
  function workspaceKnowledgeSourceFromObject(source = {}, baseCwd = "", scopedRoot = "") {
4382
+ if (typeof source === "string") {
4383
+ const ref = source.trim();
4384
+ if (!ref) return null;
4385
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(ref)) {
4386
+ return { id: ref, label: ref, kind: "binding", type: "", path: "", repoPath: "", mountPath: "", repoUrl: "", branch: "", ref, readonly: true };
4387
+ }
4388
+ source = { path: ref };
4389
+ }
4027
4390
  if (!source || typeof source !== "object" || Array.isArray(source)) return null;
4391
+ const ref = String(source.ref || source.binding || "").trim();
4028
4392
  const rawPath = String(source.path || source.repoPath || source.cwd || source.workspaceRoot || "").trim();
4029
- if (!workspaceLooksLikeKnowledgePath(rawPath)) return null;
4030
- const resolvedPath = workspaceResolvePath(baseCwd || scopedRoot, rawPath) || rawPath;
4393
+ if (!workspaceLooksLikeKnowledgePath(rawPath) && !ref) return null;
4394
+ const resolvedPath = rawPath ? (workspaceResolvePath(baseCwd || scopedRoot, rawPath) || rawPath) : "";
4031
4395
  return {
4032
- id: String(source.id || source.mountPath || source.label || path.basename(resolvedPath) || "").trim(),
4033
- label: String(source.label || source.id || source.mountPath || path.basename(resolvedPath) || "知识库").trim(),
4034
- kind: String(source.kind || (source.repoUrl ? "git" : "local")).trim() || "local",
4396
+ id: String(source.id || source.mountPath || source.label || path.basename(resolvedPath) || ref || "").trim(),
4397
+ label: String(source.label || source.id || source.mountPath || path.basename(resolvedPath) || ref || "知识库").trim(),
4398
+ kind: String(source.kind || (source.repoUrl ? "git" : (ref ? "binding" : "local"))).trim() || "local",
4035
4399
  type: String(source.type || "").trim(),
4036
4400
  path: resolvedPath,
4037
4401
  repoPath: resolvedPath,
4038
4402
  mountPath: String(source.mountPath || "").trim(),
4039
4403
  repoUrl: String(source.repoUrl || "").trim(),
4040
4404
  branch: String(source.branch || "").trim(),
4405
+ ref,
4041
4406
  readonly: source.readonly !== false,
4042
4407
  };
4043
4408
  }
@@ -4073,7 +4438,7 @@ export function workspaceKnowledgeSourcesFromText(text, baseCwd = "", scopedRoot
4073
4438
  }
4074
4439
 
4075
4440
  function workspaceKnowledgeContextBlockFromSources(sources = []) {
4076
- const valid = Array.isArray(sources) ? sources.filter((source) => source?.path || source?.repoPath) : [];
4441
+ const valid = Array.isArray(sources) ? sources.filter((source) => source?.path || source?.repoPath || source?.ref) : [];
4077
4442
  if (!valid.length) return "";
4078
4443
  const lines = [
4079
4444
  "## 知识库上下文",
@@ -4087,6 +4452,7 @@ function workspaceKnowledgeContextBlockFromSources(sources = []) {
4087
4452
  lines.push(`${index + 1}. ${label}`);
4088
4453
  if (source.kind) lines.push(` - 类型:${source.kind}${source.type ? `/${source.type}` : ""}`);
4089
4454
  if (sourcePath) lines.push(` - 路径:\`${sourcePath}\``);
4455
+ if (source.ref) lines.push(` - 绑定:\`${source.ref}\``);
4090
4456
  if (source.mountPath) lines.push(` - 挂载目录:${source.mountPath}`);
4091
4457
  if (source.repoUrl) lines.push(` - Git URL:${source.repoUrl}`);
4092
4458
  if (source.branch) lines.push(` - 分支:${source.branch}`);
@@ -4100,10 +4466,11 @@ function workspaceDedupeKnowledgeSources(sources = []) {
4100
4466
  for (const source of Array.isArray(sources) ? sources : []) {
4101
4467
  if (!source || typeof source !== "object") continue;
4102
4468
  const key = [
4103
- path.resolve(String(source.path || source.repoPath || "")),
4469
+ source.path || source.repoPath ? path.resolve(String(source.path || source.repoPath)) : "",
4104
4470
  String(source.mountPath || ""),
4105
4471
  String(source.repoUrl || ""),
4106
4472
  String(source.branch || ""),
4473
+ String(source.ref || ""),
4107
4474
  ].join("\n");
4108
4475
  if (seen.has(key)) continue;
4109
4476
  seen.add(key);
@@ -4149,16 +4516,18 @@ function workspaceGlobalKnowledgeSources(graph, scopedRoot = "", logicalCwd = ""
4149
4516
  );
4150
4517
  }
4151
4518
 
4152
- function workspaceNodeWorkspaceContextBlock(graph, nodeId, outputs, scopedRoot = "", logicalCwd = "") {
4519
+ function workspaceNodeWorkspaceContextBlock(graph, nodeId, outputs, scopedRoot = "", logicalCwd = "", contextBundle = {}) {
4153
4520
  const root = scopedRoot ? path.resolve(scopedRoot) : "";
4154
4521
  const cwd = logicalCwd ? path.resolve(logicalCwd) : root;
4155
- const knowledgeText = workspaceSemanticInputText(graph, nodeId, outputs, "knowledgeContext", scopedRoot);
4522
+ const knowledgeText = workspaceSemanticInputText(graph, nodeId, outputs, "knowledgeContext", scopedRoot)
4523
+ || String(contextBundle?.knowledgeContext || "");
4156
4524
  let knowledgeSources = workspaceKnowledgeSourcesFromText(knowledgeText, cwd || root, scopedRoot);
4157
4525
  knowledgeSources = workspaceDedupeKnowledgeSources([
4158
4526
  ...workspaceGlobalKnowledgeSources(graph, scopedRoot, logicalCwd, nodeId),
4159
4527
  ...knowledgeSources,
4160
4528
  ]);
4161
- const workspaceText = workspaceSemanticInputText(graph, nodeId, outputs, "workspaceContext", scopedRoot);
4529
+ const workspaceText = workspaceSemanticInputText(graph, nodeId, outputs, "workspaceContext", scopedRoot)
4530
+ || String(contextBundle?.workspaceContext || "");
4162
4531
  let workspaceContext = workspaceContextObjectFromText(workspaceText, cwd || root, scopedRoot);
4163
4532
  if (!knowledgeSources.length && workspaceContext?.cwd) {
4164
4533
  knowledgeSources = workspaceKnowledgeSourcesFromText(JSON.stringify([workspaceContext]), cwd || root, scopedRoot);
@@ -4384,7 +4753,7 @@ function workspaceDefaultGitRepoRoot(scopedRoot, _userCtx = {}) {
4384
4753
  return path.join(path.resolve(scopedRoot), ".workspace", "agentflow", "git-repos");
4385
4754
  }
4386
4755
 
4387
- function workspaceDefaultWorktreePath(runTmpRoot, nodeId, repoPath, branch = "") {
4756
+ function workspaceDefaultWorktreePath(scopedRoot, runId, nodeId, repoPath, branch = "") {
4388
4757
  const repoRoot = path.resolve(repoPath);
4389
4758
  const repoName = sanitizeWorktreeName(path.basename(repoRoot));
4390
4759
  const branchName = String(branch || "").trim();
@@ -4402,9 +4771,12 @@ function workspaceDefaultWorktreePath(runTmpRoot, nodeId, repoPath, branch = "")
4402
4771
  : "HEAD";
4403
4772
  }
4404
4773
  return path.join(
4405
- path.resolve(runTmpRoot),
4406
- "worktrees",
4774
+ path.resolve(scopedRoot),
4775
+ ".workspace",
4776
+ "agentflow",
4777
+ "run-workspaces",
4407
4778
  workspaceSanitizeTmpSegment(nodeId, "node"),
4779
+ workspaceSanitizeTmpSegment(runId, "run"),
4408
4780
  repoName,
4409
4781
  sanitizeWorktreeName(refLabel),
4410
4782
  );
@@ -4444,15 +4816,18 @@ function workspaceMarkAutoWorktreeCleaned(graph, entry) {
4444
4816
  return true;
4445
4817
  }
4446
4818
 
4447
- function workspaceCleanupAutoWorktrees(list, graph, emit) {
4819
+ function workspaceCleanupAutoWorktrees(list, graph, emit, { force = false } = {}) {
4820
+ const cleaned = [];
4821
+ const preserved = [];
4448
4822
  for (const entry of [...list].reverse()) {
4449
4823
  try {
4450
4824
  const result = unloadGitWorktree({
4451
4825
  repoPath: entry.repoPath,
4452
4826
  worktreePath: entry.worktreePath,
4453
- force: true,
4827
+ force,
4454
4828
  prune: true,
4455
4829
  });
4830
+ cleaned.push(result.worktreePath);
4456
4831
  emit({
4457
4832
  type: "natural",
4458
4833
  kind: "status",
@@ -4463,15 +4838,74 @@ function workspaceCleanupAutoWorktrees(list, graph, emit) {
4463
4838
  emit({ type: "graph", nodeId: entry.nodeId, graph });
4464
4839
  }
4465
4840
  } catch (e) {
4841
+ preserved.push({ ...entry, reason: e?.message || String(e) });
4466
4842
  emit({
4467
4843
  type: "natural",
4468
4844
  kind: "warning",
4469
4845
  nodeId: entry.nodeId,
4470
- text: `临时 worktree 未自动清理:${entry.worktreePath}\n原因:${e?.message || String(e)}`,
4846
+ text: `运行 worktree 已保留:${entry.worktreePath}\n原因:${e?.message || String(e)}`,
4471
4847
  });
4472
4848
  }
4473
4849
  }
4474
4850
  list.splice(0, list.length);
4851
+ return { cleaned, preserved };
4852
+ }
4853
+
4854
+ function workspaceRunManifestPath(scopedRoot, runId) {
4855
+ const id = workspaceSanitizeTmpSegment(runId, "run");
4856
+ return path.join(path.resolve(scopedRoot), ".workspace", "agentflow", "run-manifests", `${id}.json`);
4857
+ }
4858
+
4859
+ function workspaceWriteRunManifest(scopedRoot, runId, value = {}) {
4860
+ const filePath = workspaceRunManifestPath(scopedRoot, runId);
4861
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
4862
+ const previous = (() => {
4863
+ try {
4864
+ return fs.existsSync(filePath) ? JSON.parse(fs.readFileSync(filePath, "utf-8")) : {};
4865
+ } catch {
4866
+ return {};
4867
+ }
4868
+ })();
4869
+ const next = {
4870
+ version: 1,
4871
+ ...previous,
4872
+ ...value,
4873
+ runId: String(runId || previous.runId || ""),
4874
+ updatedAt: new Date().toISOString(),
4875
+ };
4876
+ const tempPath = `${filePath}.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`;
4877
+ fs.writeFileSync(tempPath, JSON.stringify(next, null, 2) + "\n", { encoding: "utf-8", mode: 0o600 });
4878
+ fs.renameSync(tempPath, filePath);
4879
+ return next;
4880
+ }
4881
+
4882
+ export function cleanupWorkspaceRunResources(scopedRoot, runId, { force = false, status = "stopped", emit = () => {} } = {}) {
4883
+ const filePath = workspaceRunManifestPath(scopedRoot, runId);
4884
+ if (!fs.existsSync(filePath)) return { cleaned: [], preserved: [], manifestPath: filePath };
4885
+ let manifest = {};
4886
+ try {
4887
+ manifest = JSON.parse(fs.readFileSync(filePath, "utf-8"));
4888
+ } catch {
4889
+ return { cleaned: [], preserved: [], manifestPath: filePath };
4890
+ }
4891
+ const resources = Array.isArray(manifest.worktrees) ? manifest.worktrees : [];
4892
+ const pending = resources.filter((entry) => entry?.repoPath && entry?.worktreePath && entry.removed !== true);
4893
+ const result = workspaceCleanupAutoWorktrees(pending.map((entry) => ({ ...entry })), null, emit, { force });
4894
+ const cleanedSet = new Set(result.cleaned.map((item) => path.resolve(item)));
4895
+ const preservedByPath = new Map(result.preserved.map((item) => [path.resolve(item.worktreePath), item]));
4896
+ const worktrees = resources.map((entry) => {
4897
+ const target = entry?.worktreePath ? path.resolve(entry.worktreePath) : "";
4898
+ if (target && cleanedSet.has(target)) return { ...entry, removed: true, removedAt: new Date().toISOString(), reason: "" };
4899
+ if (target && preservedByPath.has(target)) return { ...entry, removed: false, reason: preservedByPath.get(target).reason };
4900
+ return entry;
4901
+ });
4902
+ workspaceWriteRunManifest(scopedRoot, runId, {
4903
+ ...manifest,
4904
+ status: result.preserved.length ? `${status}:resources-preserved` : status,
4905
+ worktrees,
4906
+ finishedAt: new Date().toISOString(),
4907
+ });
4908
+ return { ...result, manifestPath: filePath };
4475
4909
  }
4476
4910
 
4477
4911
  function workspaceSanitizeTmpSegment(value, fallback = "node") {
@@ -4496,12 +4930,13 @@ function workspaceCreateNodeTmpDir(runTmpRoot, nodeId) {
4496
4930
  return dir;
4497
4931
  }
4498
4932
 
4499
- 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 } = {}) {
4500
4934
  const nodeRunDir = workspaceCreateNodeTmpDir(runTmpRoot, nodeId);
4501
4935
  const nodeTmpDir = path.join(nodeRunDir, "tmp");
4502
4936
  const legacyOutputsDir = path.join(nodeRunDir, "outputs");
4503
- const workspaceRoot = path.resolve(scopedRoot);
4504
- 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");
4505
4940
  const nodePart = workspaceSanitizeTmpSegment(nodeId || "node", "node");
4506
4941
  const outputsRel = durableOutputs ? path.posix.join("outputs", nodePart) : "outputs";
4507
4942
  const outputsDir = durableOutputs ? path.join(workspaceOutputsDir, nodePart) : legacyOutputsDir;
@@ -4531,6 +4966,7 @@ function workspaceCreateNodeRunPackage(runTmpRoot, nodeId, { scopedRoot, cwd = "
4531
4966
  outputsRel,
4532
4967
  directWorkspaceOutputs: durableOutputs,
4533
4968
  workspaceRoot,
4969
+ outputWorkspaceRoot,
4534
4970
  workspaceOutputsDir,
4535
4971
  executionCwd: cwd ? path.resolve(cwd) : workspaceRoot,
4536
4972
  resultFileRel,
@@ -5105,15 +5541,33 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
5105
5541
  emit({ type: "status", nodeId, line: `Timing ${label}: ${elapsedMs}ms`, timing: { label, elapsedMs, ...extra } });
5106
5542
  };
5107
5543
  let cwd = scopedRoot;
5544
+ const runtimeStorageRoot = path.resolve(opts.runtimeRoot || scopedRoot);
5108
5545
  const modelKey = typeof payload?.model === "string" ? payload.model.trim() : "";
5109
5546
  const runEnv = {};
5110
5547
  const runtimeEnv = (extra = {}) => runtimeEnvForUser(userCtx, { ...runEnv, ...(extra || {}) });
5111
5548
  const autoCleanupWorktrees = [];
5112
- const runTmpRoot = workspaceCreateRunTmpRoot(scopedRoot, runNodeId);
5549
+ const runTmpRoot = workspaceCreateRunTmpRoot(runtimeStorageRoot, runNodeId);
5550
+ const runtimeRunId = String(opts.runId || payload?.runId || "").trim() || runLedgerId("workspace-execution");
5551
+ const ownsRunManifest = !(Array.isArray(opts?.subflowCallStack) && opts.subflowCallStack.length);
5552
+ const persistRunManifest = (status, extra = {}) => {
5553
+ if (!ownsRunManifest) return null;
5554
+ return workspaceWriteRunManifest(runtimeStorageRoot, runtimeRunId, {
5555
+ flowId: String(payload?.flowId || ""),
5556
+ flowSource: String(payload?.flowSource || "user"),
5557
+ runNodeId,
5558
+ status,
5559
+ runtimeRoot: runTmpRoot,
5560
+ artifactRoot: path.join(runtimeStorageRoot, "outputs"),
5561
+ worktrees: autoCleanupWorktrees.map((entry) => ({ ...entry, removed: false })),
5562
+ ...extra,
5563
+ });
5564
+ };
5565
+ persistRunManifest("running", { startedAt: new Date().toISOString() });
5113
5566
  const controlBranches = new Map();
5114
5567
  const skippedNodes = new Set();
5115
5568
  const runtimePauseNodeIds = [];
5116
5569
  let deferred = null;
5570
+ let runFailure = null;
5117
5571
  const incomingControlEdgesByTarget = new Map();
5118
5572
  for (const edge of Array.isArray(graph?.edges) ? graph.edges : []) {
5119
5573
  const target = String(edge?.target || "");
@@ -5177,11 +5631,87 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
5177
5631
  continue;
5178
5632
  }
5179
5633
 
5634
+ if (defId === "context_knowledge") {
5635
+ const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
5636
+ const workspaceIds = parseWorkspaceSkillKeys(inputValues.workspaceIds || "[]");
5637
+ if (!workspaceIds.length) throw new Error(`context.knowledge ${nodeId} requires at least one Workspace ID`);
5638
+ const catalog = new Map(listConfiguredWorkspaces(root, scopedRoot, userCtx).map((entry) => [String(entry.id || ""), entry]));
5639
+ const missing = workspaceIds.filter((id) => !catalog.has(id));
5640
+ if (missing.length) throw new Error(`context.knowledge ${nodeId} cannot resolve Workspace IDs: ${missing.join(", ")}`);
5641
+ const unavailable = workspaceIds.filter((id) => catalog.get(id)?.exists === false);
5642
+ if (unavailable.length) throw new Error(`context.knowledge ${nodeId} Workspace paths are not ready: ${unavailable.join(", ")}`);
5643
+ const sources = workspaceIds
5644
+ .map((id, index) => workspaceKnowledgeSourceFromObject({ ...catalog.get(id), role: index === 0 ? "primary" : "context" }, cwd || scopedRoot, scopedRoot))
5645
+ .filter(Boolean);
5646
+ const value = JSON.stringify({ version: 1, sources });
5647
+ graph.instances[nodeId] = workspaceSetOutputSlot(instance, "knowledgeContext", value);
5648
+ publishNodeOutput(nodeId, value, { emitGraph: true });
5649
+ emit({ type: "node-done", nodeId, definitionId: defId, sourceCount: sources.length, workspaceIds });
5650
+ continue;
5651
+ }
5652
+
5653
+ if (defId === "context_skills") {
5654
+ const keys = selectedSkillKeysFromConfigSlots(instance);
5655
+ if (!keys.length) throw new Error(`context.skills ${nodeId} requires at least one skill`);
5656
+ const value = loadSkillsBlockForKeys(keys);
5657
+ graph.instances[nodeId] = workspaceSetOutputSlot(instance, "skillsContext", value);
5658
+ publishNodeOutput(nodeId, value, { emitGraph: true });
5659
+ emit({ type: "node-done", nodeId, definitionId: defId, skillCount: keys.length });
5660
+ continue;
5661
+ }
5662
+
5663
+ if (defId === "context_workspace") {
5664
+ const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
5665
+ const workspaceId = String(inputValues.workspaceId || "current").trim();
5666
+ const access = String(inputValues.access || "read-write").trim().toLowerCase();
5667
+ if (!["read-only", "read-write"].includes(access)) {
5668
+ throw new Error(`context.workspace ${nodeId} access must be read-only or read-write`);
5669
+ }
5670
+ const catalog = new Map(listConfiguredWorkspaces(root, scopedRoot, userCtx).map((entry) => [String(entry.id || ""), entry]));
5671
+ const selected = catalog.get(workspaceId);
5672
+ if (!selected) throw new Error(`context.workspace ${nodeId} cannot resolve Workspace ID ${workspaceId || "(empty)"}`);
5673
+ if (selected.exists === false) throw new Error(`context.workspace ${nodeId} Workspace path is not ready: ${workspaceId}`);
5674
+ const workspaceRoot = path.resolve(selected.path);
5675
+ const value = JSON.stringify({
5676
+ version: 1,
5677
+ workspaceId,
5678
+ access,
5679
+ label: String(selected.label || selected.id || path.basename(workspaceRoot)),
5680
+ cwd: workspaceRoot,
5681
+ workspaceRoot,
5682
+ pipelineWorkspace: path.resolve(scopedRoot),
5683
+ previous: null,
5684
+ });
5685
+ let nextInstance = workspaceSetOutputSlot(instance, "workspaceContext", value);
5686
+ graph.instances[nodeId] = nextInstance;
5687
+ publishNodeOutput(nodeId, value, { emitGraph: true });
5688
+ emit({ type: "node-done", nodeId, definitionId: defId, workspaceId, access });
5689
+ continue;
5690
+ }
5691
+
5692
+ if (defId === "context_bundle") {
5693
+ const bundle = {
5694
+ version: 1,
5695
+ knowledgeContext: workspaceSemanticInputText(graph, nodeId, outputs, "knowledgeContext", scopedRoot),
5696
+ skillsContext: workspaceSemanticInputText(graph, nodeId, outputs, "skillsContext", scopedRoot),
5697
+ workspaceContext: workspaceSemanticInputText(graph, nodeId, outputs, "workspaceContext", scopedRoot),
5698
+ mcpContext: workspaceSemanticInputText(graph, nodeId, outputs, "mcpContext", scopedRoot),
5699
+ };
5700
+ if (![bundle.knowledgeContext, bundle.skillsContext, bundle.workspaceContext, bundle.mcpContext].some((item) => String(item || "").trim())) {
5701
+ throw new Error(`context.bundle ${nodeId} requires at least one connected Context resource`);
5702
+ }
5703
+ const value = JSON.stringify(bundle);
5704
+ graph.instances[nodeId] = workspaceSetOutputSlot(instance, "context", value);
5705
+ publishNodeOutput(nodeId, value, { emitGraph: true });
5706
+ emit({ type: "node-done", nodeId, definitionId: defId });
5707
+ continue;
5708
+ }
5709
+
5180
5710
  if (defId === "control_subflow_call") {
5181
5711
  const subflowId = String(instance.subflowId || "").trim();
5182
5712
  const subflow = graph?.subflows?.[subflowId];
5183
5713
  if (!subflow) throw new Error(`flow.call ${nodeId} references missing subflow ${subflowId || "(empty)"}`);
5184
- const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
5714
+ const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot, { includeContext: true });
5185
5715
  const { resultValues, callFrameId } = await workspaceRunSubflowFrame({
5186
5716
  root,
5187
5717
  scopedRoot,
@@ -5297,6 +5827,7 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
5297
5827
 
5298
5828
  if (defId === "control_while") {
5299
5829
  const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
5830
+ const loopContext = workspaceSemanticInputText(graph, nodeId, outputs, "context", scopedRoot);
5300
5831
  const config = normalizeControlWhileConfig(inputValues);
5301
5832
  const stepScript = String(instance.script || instance.body || "").trim();
5302
5833
  const stepScriptRef = String(instance.scriptRef || "").trim();
@@ -5392,6 +5923,7 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
5392
5923
  parentDefinitionId: defId,
5393
5924
  subflowId: whileSubflows.conditionId,
5394
5925
  inputValues: {
5926
+ ...(loopContext && whileSubflows.condition.inputs?.context ? { context: loopContext } : {}),
5395
5927
  state: stateText,
5396
5928
  iteration: String(iteration),
5397
5929
  idempotencyKey,
@@ -5417,6 +5949,7 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
5417
5949
  parentDefinitionId: defId,
5418
5950
  subflowId: whileSubflows.bodyId,
5419
5951
  inputValues: {
5952
+ ...(loopContext && whileSubflows.body.inputs?.context ? { context: loopContext } : {}),
5420
5953
  state: stateText,
5421
5954
  iteration: String(iteration),
5422
5955
  idempotencyKey,
@@ -5437,7 +5970,8 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
5437
5970
  });
5438
5971
  }
5439
5972
  const runPackage = workspaceCreateNodeRunPackage(runTmpRoot, `${nodeId}-iteration-${iteration}`, {
5440
- scopedRoot,
5973
+ scopedRoot: runtimeStorageRoot,
5974
+ sourceRoot: scopedRoot,
5441
5975
  cwd,
5442
5976
  task: stepScript || stepScriptRef,
5443
5977
  inputValues: iterationInputs,
@@ -5811,9 +6345,17 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
5811
6345
  const worktreeInputSlot = (Array.isArray(instance.input) ? instance.input : [])
5812
6346
  .find((slot) => String(slot?.name || "") === "worktreePath") || null;
5813
6347
  const rawWorktreePath = workspaceSlotValue(worktreeInputSlot || workspaceSlotByName(instance, "worktreePath")).trim();
6348
+ const retainedWorktreePath = workspaceSlotValue(
6349
+ (Array.isArray(instance.output) ? instance.output : [])
6350
+ .find((slot) => String(slot?.name || "") === "worktreePath"),
6351
+ ).trim();
5814
6352
  const worktreePath = rawWorktreePath
5815
6353
  ? workspaceResolvePath(cwd, rawWorktreePath)
5816
- : (gitContext?.worktreePath ? path.resolve(gitContext.worktreePath) : workspaceDefaultWorktreePath(runTmpRoot, nodeId, repoPath, branch));
6354
+ : (gitContext?.worktreePath
6355
+ ? path.resolve(gitContext.worktreePath)
6356
+ : (retainedWorktreePath
6357
+ ? path.resolve(retainedWorktreePath)
6358
+ : workspaceDefaultWorktreePath(scopedRoot, runtimeRunId, nodeId, repoPath, branch)));
5817
6359
  const previousCwd = cwd;
5818
6360
  const force = ["true", "1", "yes", "on"].includes(workspaceSlotValue(workspaceSlotByName(instance, "force")).trim().toLowerCase());
5819
6361
  const pruneMissingRaw = workspaceSlotValue(workspaceSlotByName(instance, "pruneMissing")).trim().toLowerCase();
@@ -5825,6 +6367,7 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
5825
6367
  repoPath: result.repoRoot,
5826
6368
  worktreePath: result.worktreePath,
5827
6369
  });
6370
+ persistRunManifest("running");
5828
6371
  }
5829
6372
  const outGitContext = buildGitContext({
5830
6373
  repoPath: result.repoRoot,
@@ -6006,7 +6549,8 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
6006
6549
  const prepareStartedAt = Date.now();
6007
6550
  const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
6008
6551
  const runPackage = workspaceCreateNodeRunPackage(runTmpRoot, nodeId, {
6009
- scopedRoot,
6552
+ scopedRoot: runtimeStorageRoot,
6553
+ sourceRoot: scopedRoot,
6010
6554
  cwd,
6011
6555
  task: String(instance.script || instance.scriptRef || instance.body || "").trim(),
6012
6556
  inputValues,
@@ -6067,13 +6611,21 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
6067
6611
  const relevantInputs = workspaceRelevantInputValues(instance.body || "", inputValues);
6068
6612
  workspaceAssertRequiredInputs(instance.body || "", inputValues, nodeId);
6069
6613
  const upstreamText = workspaceTaskUpstreamText(graph, nodeId, outputs, relevantInputs.placeholders, scopedRoot);
6070
- const upstreamSkillBlocks = workspaceUpstreamSkillBlocks(graph, nodeId, outputs);
6614
+ const contextBundle = workspaceNodeContextBundle(graph, nodeId, outputs, scopedRoot);
6615
+ const upstreamSkillBlocks = mergeWorkspaceSkillBlocks(
6616
+ workspaceUpstreamSkillBlocks(graph, nodeId, outputs),
6617
+ String(contextBundle.skillsContext || ""),
6618
+ );
6071
6619
  const ownSkillBlock = isContextRunNode ? loadSkillsBlockForKeys(selectedSkillKeysFromConfigSlots(instance)) : "";
6072
6620
  const promptSkillsBlock = mergeWorkspaceSkillBlocks(ownSkillBlock, upstreamSkillBlocks);
6073
- const promptMcpBlock = workspaceUpstreamMcpBlocks(graph, nodeId, outputs);
6621
+ const promptMcpBlock = mergeWorkspaceSkillBlocks(
6622
+ workspaceUpstreamMcpBlocks(graph, nodeId, outputs),
6623
+ String(contextBundle.mcpContext || ""),
6624
+ );
6074
6625
  const resultOutputSpec = workspaceResultOutputSpec(graph, nodeId);
6075
6626
  const runPackage = workspaceCreateNodeRunPackage(runTmpRoot, nodeId, {
6076
- scopedRoot,
6627
+ scopedRoot: runtimeStorageRoot,
6628
+ sourceRoot: scopedRoot,
6077
6629
  cwd,
6078
6630
  task: workspaceResolveBodyPlaceholders(instance.body || "", inputValues).trim() || upstreamText,
6079
6631
  inputValues: relevantInputs.values,
@@ -6095,7 +6647,7 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
6095
6647
  // Best-effort debug artifact only.
6096
6648
  }
6097
6649
  const historyBlock = workspaceNodeHistoryBlock(nodeId, scopedRoot, runPackage);
6098
- let workspaceContextBlock = workspaceNodeWorkspaceContextBlock(graph, nodeId, outputs, scopedRoot, cwd);
6650
+ let workspaceContextBlock = workspaceNodeWorkspaceContextBlock(graph, nodeId, outputs, scopedRoot, cwd, contextBundle);
6099
6651
  if (!isContextRunNode && workspaceBoolSlot(instance, "includeWorkspaceContext", true)) {
6100
6652
  const defaultWorkspaceBlock = workspaceDefaultWorkspaceContextBlock(scopedRoot, cwd);
6101
6653
  if (!workspaceContextBlock) {
@@ -6229,9 +6781,47 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
6229
6781
  outputFiles: normalizedAgentOutput.outputFiles || [],
6230
6782
  });
6231
6783
  }
6784
+ } catch (error) {
6785
+ runFailure = error;
6786
+ throw error;
6232
6787
  } finally {
6233
- workspaceCleanupAutoWorktrees(autoCleanupWorktrees, graph, emit);
6234
- workspaceCleanupTmpRoot(runTmpRoot, userCtx, emit);
6788
+ const retainForResume = Boolean(deferred || runtimePauseNodeIds.length);
6789
+ const trackedWorktrees = autoCleanupWorktrees.map((entry) => ({ ...entry }));
6790
+ let cleanup = { cleaned: [], preserved: [] };
6791
+ if (retainForResume) {
6792
+ emit({
6793
+ type: "status",
6794
+ line: `Run workspace retained for resume${trackedWorktrees.length ? ` (${trackedWorktrees.length} worktree)` : ""}`,
6795
+ runId: runtimeRunId,
6796
+ });
6797
+ persistRunManifest("waiting", {
6798
+ worktrees: trackedWorktrees.map((entry) => ({ ...entry, removed: false })),
6799
+ waitingAt: new Date().toISOString(),
6800
+ });
6801
+ autoCleanupWorktrees.splice(0, autoCleanupWorktrees.length);
6802
+ } else {
6803
+ cleanup = workspaceCleanupAutoWorktrees(autoCleanupWorktrees, graph, emit, { force: false });
6804
+ const cleanedSet = new Set(cleanup.cleaned.map((item) => path.resolve(item)));
6805
+ const preservedByPath = new Map(cleanup.preserved.map((item) => [path.resolve(item.worktreePath), item]));
6806
+ persistRunManifest(runFailure ? "failed" : "completed", {
6807
+ worktrees: trackedWorktrees.map((entry) => {
6808
+ const target = path.resolve(entry.worktreePath);
6809
+ if (cleanedSet.has(target)) return { ...entry, removed: true, removedAt: new Date().toISOString(), reason: "" };
6810
+ if (preservedByPath.has(target)) return { ...entry, removed: false, reason: preservedByPath.get(target).reason };
6811
+ return entry;
6812
+ }),
6813
+ finishedAt: new Date().toISOString(),
6814
+ error: runFailure ? (runFailure?.message || String(runFailure)) : "",
6815
+ resourcesPreserved: cleanup.preserved.length > 0,
6816
+ });
6817
+ }
6818
+ const protectedWorktrees = retainForResume ? trackedWorktrees : cleanup.preserved;
6819
+ const protectsRunTmpRoot = protectedWorktrees.some((entry) => workspacePathInside(runTmpRoot, entry.worktreePath));
6820
+ if (protectsRunTmpRoot) {
6821
+ emit({ type: "status", line: `Workspace tmp kept because it contains a retained worktree: ${runTmpRoot}` });
6822
+ } else {
6823
+ workspaceCleanupTmpRoot(runTmpRoot, userCtx, emit);
6824
+ }
6235
6825
  }
6236
6826
  const finalPauseNodeIds = Array.from(new Set([...pauseNodeIds, ...runtimePauseNodeIds]));
6237
6827
  if (!deferred && finalPauseNodeIds.length > 0) {
@@ -6444,6 +7034,17 @@ export function upsertWorkspaceDeferredRun(meta = {}, deferred = {}) {
6444
7034
  nodeId: String(deferred.nodeId || meta.nodeId || previous.nodeId || ""),
6445
7035
  label: String(meta.label || previous.label || "Workspace Run"),
6446
7036
  plannedNodeIds: Array.isArray(meta.plannedNodeIds) ? meta.plannedNodeIds.map(String) : (previous.plannedNodeIds || []),
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 || ""),
7041
+ marketplaceResources: Array.isArray(meta.marketplaceResources)
7042
+ ? meta.marketplaceResources.map((item) => ({
7043
+ kind: String(item?.kind || ""),
7044
+ id: String(item?.id || ""),
7045
+ version: String(item?.version || ""),
7046
+ })).filter((item) => item.kind && item.id && item.version)
7047
+ : (previous.marketplaceResources || []),
6447
7048
  startedAt: Number(meta.startedAt || previous.startedAt || now),
6448
7049
  scheduled: meta.scheduled === true || previous.scheduled === true,
6449
7050
  scheduleKey: String(meta.scheduleKey || previous.scheduleKey || ""),
@@ -6639,7 +7240,8 @@ export function listWorkspaceScheduleStatuses(root, userCtx = {}) {
6639
7240
  if (scoped.error || !scoped.root) continue;
6640
7241
  let graph;
6641
7242
  try {
6642
- graph = readWorkspaceGraph(scoped.root, root).graph;
7243
+ graph = readWorkspaceStableRelease(scoped.root, root)?.graph
7244
+ || readWorkspaceGraph(scoped.root, root).graph;
6643
7245
  } catch {
6644
7246
  continue;
6645
7247
  }
@@ -6669,6 +7271,9 @@ export function listWorkspaceScheduleStatuses(root, userCtx = {}) {
6669
7271
  rows.push({
6670
7272
  kind: "workspace",
6671
7273
  key,
7274
+ registered: Boolean(registry.schedules?.[key]),
7275
+ ownerUserId: scheduleUserId,
7276
+ ownerUsername: String(current.username || scheduleUserId),
6672
7277
  flowId,
6673
7278
  flowSource,
6674
7279
  workspaceId: String(flow.collaboration?.id || scoped.workspaceId || ""),
@@ -6732,11 +7337,12 @@ export function syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, us
6732
7337
  writeWorkspaceScheduleRegistry({ version: 1, schedules });
6733
7338
  return [];
6734
7339
  }
6735
- 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 : {};
6736
7342
  for (const [scheduleNodeId, instance] of Object.entries(instances)) {
6737
7343
  if (String(instance?.definitionId || "") !== "workspace_scheduled_run") continue;
6738
7344
  const config = normalizeWorkspaceScheduledRunConfig(instance.body || "");
6739
- const targetRunNodeId = workspaceScheduleInferTargetRunNodeId(graph, scheduleNodeId, config);
7345
+ const targetRunNodeId = workspaceScheduleInferTargetRunNodeId(effectiveGraph, scheduleNodeId, config);
6740
7346
  const key = workspaceScheduleKey(userId, flowSource, flowId, scheduleNodeId);
6741
7347
  const previous = registry.schedules?.[key] && typeof registry.schedules[key] === "object" ? registry.schedules[key] : {};
6742
7348
  const previousNext = Number(previous.nextRunAt || 0);
@@ -6857,7 +7463,15 @@ export async function runWorkspaceScheduledEntry(root, entry) {
6857
7463
  });
6858
7464
  return;
6859
7465
  }
6860
- 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
+ );
6861
7475
  const scheduleNodeId = String(entry.scheduleNodeId || entry.key?.split(":").pop() || "");
6862
7476
  const instance = graph.instances?.[scheduleNodeId];
6863
7477
  const config = normalizeWorkspaceScheduledRunConfig(instance?.body || "");
@@ -6891,7 +7505,7 @@ export async function runWorkspaceScheduledEntry(root, entry) {
6891
7505
  appendWorkspaceRunLogEvent(runLog.runId, { type: "scheduler-triggered", scheduleNodeId, runNodeId: targetRunNodeId, cron: config.cron, timezone: config.timezone });
6892
7506
  let plan;
6893
7507
  try {
6894
- plan = workspaceRunPlan(graph, targetRunNodeId, scoped.root);
7508
+ plan = workspaceRunPlan(graph, targetRunNodeId, executionRoot);
6895
7509
  } catch (e) {
6896
7510
  const error = (e && e.message) || String(e);
6897
7511
  appendWorkspaceRunLogEvent(runLog.runId, { type: "error", error });
@@ -6943,7 +7557,18 @@ export async function runWorkspaceScheduledEntry(root, entry) {
6943
7557
  plannedNodeIds,
6944
7558
  startedAt: Date.now(),
6945
7559
  scheduled: true,
7560
+ workspaceRoot: root,
7561
+ executionRoot,
7562
+ releaseId: stableRelease?.release?.id || "",
7563
+ designRevision: stableRelease?.release?.designRevision || workspaceDesignRevision(graph),
7564
+ marketplaceResources: marketplaceResourcesForRun(executionRoot, graph, plannedNodeIds),
6946
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
+ });
6947
7572
  activeWorkspaceRuns.set(runKey, runEntry);
6948
7573
  appendWorkspaceRunStarted(runEntry);
6949
7574
  updateWorkspaceScheduleEntry(entry.key, {
@@ -6960,12 +7585,13 @@ export async function runWorkspaceScheduledEntry(root, entry) {
6960
7585
  runControl.setChild(child, childOptions);
6961
7586
  };
6962
7587
  try {
6963
- const result = await runWorkspaceGraph(root, scoped.root, {
7588
+ const result = await runWorkspaceGraph(root, executionRoot, {
6964
7589
  flowId: entry.flowId,
6965
7590
  flowSource: entry.flowSource || "user",
6966
7591
  runNodeId: targetRunNodeId,
6967
7592
  graph,
6968
7593
  }, userCtx, {
7594
+ runtimeRoot: scoped.root,
6969
7595
  signal: controller.signal,
6970
7596
  onActiveChild: setActiveChild,
6971
7597
  onEvent: (event) => appendWorkspaceRunLogEvent(runLog.runId, event),
@@ -6973,7 +7599,12 @@ export async function runWorkspaceScheduledEntry(root, entry) {
6973
7599
  });
6974
7600
  const currentGraph = readWorkspaceGraph(scoped.root, root).graph;
6975
7601
  const touchedIds = workspaceRunTouchedNodeIds(result);
6976
- 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);
6977
7608
  writeWorkspaceGraph(scoped.root, mergedGraph, root);
6978
7609
  if (result.deferred) {
6979
7610
  const waiting = upsertWorkspaceDeferredRun({
@@ -6997,11 +7628,18 @@ export async function runWorkspaceScheduledEntry(root, entry) {
6997
7628
  return;
6998
7629
  }
6999
7630
  const endedAt = Date.now();
7000
- appendWorkspaceRunFinished({ ...runEntry, endedAt, durationMs: endedAt - runEntry.startedAt }, "success");
7631
+ appendWorkspaceRunFinished({
7632
+ ...runEntry,
7633
+ endedAt,
7634
+ durationMs: endedAt - runEntry.startedAt,
7635
+ marketplaceResources: marketplaceResourcesForRun(executionRoot, graph, result.order || Array.from(touchedIds)),
7636
+ }, "success");
7001
7637
  finishWorkspaceRunLogSession(runLog.runId, "success", {
7002
7638
  endedAt,
7003
7639
  durationMs: endedAt - runEntry.startedAt,
7004
7640
  runNodeId: targetRunNodeId,
7641
+ releaseId: runEntry.releaseId,
7642
+ designRevision: runEntry.designRevision,
7005
7643
  });
7006
7644
  updateWorkspaceScheduleEntry(entry.key, {
7007
7645
  nextRunAt: computeNext(config),
@@ -7022,6 +7660,8 @@ export async function runWorkspaceScheduledEntry(root, entry) {
7022
7660
  endedAt,
7023
7661
  durationMs: endedAt - runEntry.startedAt,
7024
7662
  runNodeId: targetRunNodeId,
7663
+ releaseId: runEntry.releaseId,
7664
+ designRevision: runEntry.designRevision,
7025
7665
  error: stopped ? "" : error,
7026
7666
  });
7027
7667
  updateWorkspaceScheduleEntry(entry.key, {
@@ -7049,6 +7689,8 @@ function finishWorkspaceDeferredRun(entry, status, patch = {}) {
7049
7689
  endedAt,
7050
7690
  durationMs: Math.max(0, endedAt - Number(entry.startedAt || endedAt)),
7051
7691
  runNodeId: entry.runNodeId || "",
7692
+ releaseId: entry.releaseId || "",
7693
+ designRevision: entry.designRevision || "",
7052
7694
  error: String(patch.error || ""),
7053
7695
  });
7054
7696
  if (entry.scheduleKey) {
@@ -7094,13 +7736,16 @@ async function runWorkspaceDeferredEntry(root, claimed) {
7094
7736
  if (activeWorkspaceRuns.get(runKey) === runEntry) activeWorkspaceRuns.delete(runKey);
7095
7737
  };
7096
7738
  try {
7097
- const graph = hydrateWorkspaceGraphForRuntime(root, scoped, readWorkspaceGraph(scoped.root, root).graph, userCtx);
7098
- 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, {
7099
7743
  flowId: claimed.flowId,
7100
7744
  flowSource: claimed.flowSource || "user",
7101
7745
  runNodeId: claimed.runNodeId,
7102
7746
  graph,
7103
7747
  }, userCtx, {
7748
+ runtimeRoot: scoped.root,
7104
7749
  signal: controller.signal,
7105
7750
  onActiveChild: (child, options = {}) => runControl.setChild(child, options),
7106
7751
  onEvent: (event) => appendWorkspaceRunLogEvent(claimed.runId, event),
@@ -7108,7 +7753,12 @@ async function runWorkspaceDeferredEntry(root, claimed) {
7108
7753
  });
7109
7754
  const currentGraph = readWorkspaceGraph(scoped.root, root).graph;
7110
7755
  const touchedIds = workspaceRunTouchedNodeIds(result);
7111
- 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);
7112
7762
  writeWorkspaceGraph(scoped.root, mergedGraph, root);
7113
7763
  if (result.deferred) {
7114
7764
  const waiting = upsertWorkspaceDeferredRun({ ...claimed, deferredKey: claimed.key }, result.deferred);