botmux-workflow-core 3.16.0 → 3.17.0

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.
@@ -267,6 +267,104 @@ function isRecord(value) {
267
267
  return typeof value === "object" && value !== null && !Array.isArray(value);
268
268
  }
269
269
 
270
+ // src/workflows/v3/artifact-contract.ts
271
+ var MANIFEST_FILE_KINDS = [
272
+ "markdown",
273
+ "json",
274
+ "text",
275
+ "code",
276
+ "log",
277
+ "binary",
278
+ "directory"
279
+ ];
280
+ var MANIFEST_SUMMARY_MAX_BYTES = 4 * 1024;
281
+ var MANIFEST_PREVIEW_MAX_BYTES = 4 * 1024;
282
+ var MANIFEST_STATUSES = ["ok", "fail"];
283
+ var MANIFEST_SCHEMA_VERSION = 1;
284
+
285
+ // src/workflows/v3/artifact-contract-declarations.ts
286
+ var V3_ARTIFACT_OUTPUT_KEY_RE = /^[A-Za-z0-9._-]+$/;
287
+ var V3_ARTIFACT_OUTPUT_MAX_COUNT = 32;
288
+ var V3_ARTIFACT_OUTPUT_MAX_BYTES = 4 * 1024;
289
+ function normalizeArtifactOutputs(value, where, problems) {
290
+ if (value === void 0) return void 0;
291
+ if (!isRecord2(value) || Object.keys(value).length === 0) {
292
+ problems.push(`${where}.outputs must be a non-empty object when present`);
293
+ return void 0;
294
+ }
295
+ const entries = Object.entries(value);
296
+ if (entries.length > V3_ARTIFACT_OUTPUT_MAX_COUNT) {
297
+ problems.push(`${where}.outputs has ${entries.length} entries (max ${V3_ARTIFACT_OUTPUT_MAX_COUNT})`);
298
+ }
299
+ if (Buffer.byteLength(JSON.stringify(value), "utf-8") > V3_ARTIFACT_OUTPUT_MAX_BYTES) {
300
+ problems.push(`${where}.outputs exceeds ${V3_ARTIFACT_OUTPUT_MAX_BYTES} serialized bytes`);
301
+ }
302
+ const out = /* @__PURE__ */ Object.create(null);
303
+ const paths = /* @__PURE__ */ new Set();
304
+ for (const [key, raw] of entries) {
305
+ const itemWhere = `${where}.outputs.${JSON.stringify(key)}`;
306
+ if (!V3_ARTIFACT_OUTPUT_KEY_RE.test(key)) {
307
+ problems.push(`${itemWhere} key must match ${V3_ARTIFACT_OUTPUT_KEY_RE}`);
308
+ continue;
309
+ }
310
+ if (!isRecord2(raw)) {
311
+ problems.push(`${itemWhere} must be { path, kind }`);
312
+ continue;
313
+ }
314
+ const extra = Object.keys(raw).filter((field) => field !== "path" && field !== "kind");
315
+ if (extra.length > 0) {
316
+ problems.push(`${itemWhere} has unsupported key(s): ${extra.join(", ")} (allowed: path, kind)`);
317
+ continue;
318
+ }
319
+ if (!isPortableRelativeArtifactPath(raw.path)) {
320
+ problems.push(`${itemWhere}.path must be a portable relative path without '.', '..', or empty segments`);
321
+ continue;
322
+ }
323
+ if (typeof raw.kind !== "string" || !MANIFEST_FILE_KINDS.includes(raw.kind)) {
324
+ problems.push(`${itemWhere}.kind must be one of ${MANIFEST_FILE_KINDS.join(" | ")}`);
325
+ continue;
326
+ }
327
+ if (paths.has(raw.path)) {
328
+ problems.push(`${where}.outputs contains duplicate path ${JSON.stringify(raw.path)}`);
329
+ continue;
330
+ }
331
+ paths.add(raw.path);
332
+ out[key] = { path: raw.path, kind: raw.kind };
333
+ }
334
+ return out;
335
+ }
336
+ function validateManifestArtifactContract(outputs, manifest) {
337
+ if (!outputs) return { ok: true, problems: [] };
338
+ const problems = [];
339
+ for (const [key, declaration] of Object.entries(outputs)) {
340
+ const matches = manifest.files.filter((file) => file.path === declaration.path);
341
+ if (matches.length === 0) {
342
+ problems.push(
343
+ `output ${JSON.stringify(key)} requires path ${JSON.stringify(declaration.path)} (available: ${manifest.files.map((file) => `${file.path}:${file.kind}`).join(", ") || "none"})`
344
+ );
345
+ continue;
346
+ }
347
+ if (matches.length > 1) {
348
+ problems.push(`output ${JSON.stringify(key)} path ${JSON.stringify(declaration.path)} appears more than once`);
349
+ continue;
350
+ }
351
+ if (matches[0].kind !== declaration.kind) {
352
+ problems.push(
353
+ `output ${JSON.stringify(key)} path ${JSON.stringify(declaration.path)} has kind ${JSON.stringify(matches[0].kind)}, expected ${JSON.stringify(declaration.kind)}`
354
+ );
355
+ }
356
+ }
357
+ return { ok: problems.length === 0, problems };
358
+ }
359
+ function isPortableRelativeArtifactPath(value) {
360
+ if (typeof value !== "string" || value.length === 0 || value.startsWith("/") || value.includes("\\") || value.includes("\0") || /^[A-Za-z]:/.test(value)) return false;
361
+ const segments = value.split("/");
362
+ return segments.every((segment) => segment !== "" && segment !== "." && segment !== "..");
363
+ }
364
+ function isRecord2(value) {
365
+ return typeof value === "object" && value !== null && !Array.isArray(value);
366
+ }
367
+
270
368
  // src/workflows/v3/dag.ts
271
369
  var NODE_KINDS = ["goal", "host", "loop"];
272
370
  var V3_HOST_EXECUTORS = ["feishu-send", "feishu-reply", "botmux-schedule"];
@@ -317,6 +415,10 @@ function validateDag(raw) {
317
415
  if (!isObject(raw)) {
318
416
  throw new DagValidationError(["root must be a JSON object"]);
319
417
  }
418
+ const schemaVersion = raw.schemaVersion === void 0 ? 1 : raw.schemaVersion;
419
+ if (schemaVersion !== 1 && schemaVersion !== 2) {
420
+ problems.push(`schemaVersion must be 1 or 2 (got ${JSON.stringify(raw.schemaVersion)})`);
421
+ }
320
422
  if (typeof raw.runId !== "string" || !SEGMENT_RE.test(raw.runId)) {
321
423
  problems.push(`runId must be a path-safe string matching ${SEGMENT_RE} (got ${JSON.stringify(raw.runId)})`);
322
424
  }
@@ -353,9 +455,9 @@ function validateDag(raw) {
353
455
  if (fromList.includes(id)) problems.push(`node "${id}" depends on itself`);
354
456
  if (new Set(fromList).size !== fromList.length) problems.push(`node "${id}".depends has duplicates`);
355
457
  const triggerRule = normTriggerRule(n.triggerRule, depends.length, `node "${id}"`, problems);
356
- const inputs = normInputs(n.inputs, id, problems);
458
+ const inputs = normInputs(n.inputs, id, problems, schemaVersion === 2 ? 2 : 1);
357
459
  if (type === "loop") {
358
- const loopFields = normLoopFields(n, id, problems);
460
+ const loopFields = normLoopFields(n, id, problems, schemaVersion === 2 ? 2 : 1);
359
461
  if (loopFields) {
360
462
  nodes.push({
361
463
  id,
@@ -372,6 +474,7 @@ function validateDag(raw) {
372
474
  continue;
373
475
  }
374
476
  if (type === "host") {
477
+ if (n.outputs !== void 0) problems.push(`host node "${id}".outputs is not supported`);
375
478
  if (n.goal !== void 0) problems.push(`host node "${id}".goal is not supported`);
376
479
  if (n.bot !== void 0) problems.push(`host node "${id}".bot is not supported \u2014 host nodes do not spawn a CLI`);
377
480
  if (n.override !== void 0) problems.push(`host node "${id}".override is not supported`);
@@ -447,6 +550,10 @@ function validateDag(raw) {
447
550
  const humanGate = normHumanGate(n.humanGate, `node "${id}"`, problems);
448
551
  const override = normOverride(n.override, `node "${id}"`, problems);
449
552
  const revisitTo = normRevisitTo(n.revisitTo, id, problems);
553
+ if (schemaVersion !== 2 && n.outputs !== void 0) {
554
+ problems.push(`goal node "${id}".outputs requires schemaVersion 2`);
555
+ }
556
+ const outputs = schemaVersion === 2 ? normalizeArtifactOutputs(n.outputs, `goal node "${id}"`, problems) : void 0;
450
557
  nodes.push({
451
558
  id,
452
559
  type,
@@ -456,6 +563,7 @@ function validateDag(raw) {
456
563
  triggerRule,
457
564
  override,
458
565
  inputs,
566
+ outputs,
459
567
  timeoutSec,
460
568
  humanGate,
461
569
  resultSchema,
@@ -471,6 +579,14 @@ function validateDag(raw) {
471
579
  problems.push(`node "${node.id}".inputs references unknown node "${inp.from}"`);
472
580
  } else if (!node.depends.some((d) => d.from === inp.from)) {
473
581
  problems.push(`node "${node.id}".inputs.from "${inp.from}" must also be in depends`);
582
+ } else if (inp.output !== void 0) {
583
+ const source = nodes.find((candidate) => candidate.id === inp.from);
584
+ const outputs = source?.type === "loop" ? source.body?.nodes.find((bodyNode) => bodyNode.id === source.output?.from)?.outputs : source?.outputs;
585
+ if (!outputs || !Object.prototype.hasOwnProperty.call(outputs, inp.output)) {
586
+ problems.push(
587
+ `node "${node.id}".inputs output ${JSON.stringify(inp.output)} is not declared by source "${inp.from}"`
588
+ );
589
+ }
474
590
  }
475
591
  }
476
592
  if (node.type === "host") {
@@ -549,7 +665,11 @@ function validateDag(raw) {
549
665
  }
550
666
  }
551
667
  if (problems.length > 0) throw new DagValidationError(problems);
552
- const dag = { runId: raw.runId, nodes };
668
+ const dag = {
669
+ ...raw.schemaVersion !== void 0 ? { schemaVersion } : {},
670
+ runId: raw.runId,
671
+ nodes
672
+ };
553
673
  topologicalOrder(dag);
554
674
  return dag;
555
675
  }
@@ -863,7 +983,7 @@ function downstreamCone(nodeId, nodes) {
863
983
  function nodeByIdUnsafe(nodes, nodeId) {
864
984
  return nodes.find((node) => node.id === nodeId);
865
985
  }
866
- function normLoopFields(n, id, problems) {
986
+ function normLoopFields(n, id, problems, schemaVersion) {
867
987
  const where = `loop node "${id}"`;
868
988
  const before = problems.length;
869
989
  if (n.timeoutSec !== void 0) {
@@ -934,10 +1054,14 @@ function normLoopFields(n, id, problems) {
934
1054
  const bFromList = bdepends.map((d) => d.from);
935
1055
  if (bFromList.includes(bid)) problems.push(`${where}.body node "${bid}" depends on itself`);
936
1056
  if (new Set(bFromList).size !== bFromList.length) problems.push(`${where}.body node "${bid}".depends has duplicates`);
937
- const binputs = normInputs(b.inputs, `${id}.body.${bid}`, problems);
1057
+ const binputs = normInputs(b.inputs, `${id}.body.${bid}`, problems, schemaVersion);
938
1058
  const btimeout = normTimeoutSec(b.timeoutSec, `${where}.body node "${bid}"`, problems);
939
1059
  const bschema = normResultSchema(b.resultSchema, `${id}.body.${bid}`, problems);
940
1060
  const boverride = normOverride(b.override, `${where}.body node "${bid}"`, problems);
1061
+ if (schemaVersion !== 2 && b.outputs !== void 0) {
1062
+ problems.push(`${where}.body node "${bid}".outputs requires schemaVersion 2`);
1063
+ }
1064
+ const boutputs = schemaVersion === 2 ? normalizeArtifactOutputs(b.outputs, `${where}.body node "${bid}"`, problems) : void 0;
941
1065
  bodyNodes.push({
942
1066
  id: bid,
943
1067
  type: "goal",
@@ -946,6 +1070,7 @@ function normLoopFields(n, id, problems) {
946
1070
  depends: bdepends,
947
1071
  override: boverride,
948
1072
  inputs: binputs,
1073
+ outputs: boutputs,
949
1074
  timeoutSec: btimeout,
950
1075
  humanGate: null,
951
1076
  resultSchema: bschema
@@ -960,6 +1085,13 @@ function normLoopFields(n, id, problems) {
960
1085
  problems.push(`${where}.body node "${bn.id}".inputs references unknown body node "${inp.from}"`);
961
1086
  } else if (!bn.depends.some((d) => d.from === inp.from)) {
962
1087
  problems.push(`${where}.body node "${bn.id}".inputs.from "${inp.from}" must also be in depends`);
1088
+ } else if (inp.output !== void 0) {
1089
+ const source = bodyNodes.find((candidate) => candidate.id === inp.from);
1090
+ if (!source?.outputs || !Object.prototype.hasOwnProperty.call(source.outputs, inp.output)) {
1091
+ problems.push(
1092
+ `${where}.body node "${bn.id}".inputs output ${JSON.stringify(inp.output)} is not declared by source "${inp.from}"`
1093
+ );
1094
+ }
963
1095
  }
964
1096
  }
965
1097
  }
@@ -1185,7 +1317,7 @@ function normResultSchema(v, id, problems) {
1185
1317
  }
1186
1318
  return schema;
1187
1319
  }
1188
- function normInputs(v, id, problems) {
1320
+ function normInputs(v, id, problems, schemaVersion) {
1189
1321
  if (v === void 0) return [];
1190
1322
  if (!Array.isArray(v)) {
1191
1323
  problems.push(`node "${id}".inputs must be an array`);
@@ -1198,9 +1330,29 @@ function normInputs(v, id, problems) {
1198
1330
  problems.push(`node "${id}".inputs[${j}] must be { from: <nodeId>, select? }`);
1199
1331
  continue;
1200
1332
  }
1201
- const extra = Object.keys(inp).filter((k) => k !== "from" && k !== "select");
1333
+ const extra = Object.keys(inp).filter((k) => k !== "from" && k !== "select" && k !== "output");
1202
1334
  if (extra.length > 0) {
1203
- problems.push(`node "${id}".inputs[${j}] has unsupported key(s): ${extra.join(", ")} (allowed: from, select)`);
1335
+ problems.push(`node "${id}".inputs[${j}] has unsupported key(s): ${extra.join(", ")} (allowed: from, select/output)`);
1336
+ continue;
1337
+ }
1338
+ if (schemaVersion === 2) {
1339
+ if (inp.select !== void 0) {
1340
+ problems.push(`node "${id}".inputs[${j}].select is legacy-only; schemaVersion 2 must use output`);
1341
+ continue;
1342
+ }
1343
+ if (inp.output === void 0) {
1344
+ out.push({ from: inp.from });
1345
+ continue;
1346
+ }
1347
+ if (typeof inp.output !== "string" || !SEGMENT_RE.test(inp.output)) {
1348
+ problems.push(`node "${id}".inputs[${j}].output must be a stable key matching ${SEGMENT_RE}`);
1349
+ continue;
1350
+ }
1351
+ out.push({ from: inp.from, output: inp.output });
1352
+ continue;
1353
+ }
1354
+ if (inp.output !== void 0) {
1355
+ problems.push(`node "${id}".inputs[${j}].output requires schemaVersion 2`);
1204
1356
  continue;
1205
1357
  }
1206
1358
  if (inp.select === void 0) {
@@ -3213,21 +3365,6 @@ function writePendingWait(runDir, input) {
3213
3365
  return wait;
3214
3366
  }
3215
3367
 
3216
- // src/workflows/v3/artifact-contract.ts
3217
- var MANIFEST_FILE_KINDS = [
3218
- "markdown",
3219
- "json",
3220
- "text",
3221
- "code",
3222
- "log",
3223
- "binary",
3224
- "directory"
3225
- ];
3226
- var MANIFEST_SUMMARY_MAX_BYTES = 4 * 1024;
3227
- var MANIFEST_PREVIEW_MAX_BYTES = 4 * 1024;
3228
- var MANIFEST_STATUSES = ["ok", "fail"];
3229
- var MANIFEST_SCHEMA_VERSION = 1;
3230
-
3231
3368
  // src/workflows/v3/contract.ts
3232
3369
  var GOAL_ENV = {
3233
3370
  /** Path to the single-sentence goal text file. */
@@ -3696,7 +3833,7 @@ function readAndVerifyV3HostSuccessResult(input) {
3696
3833
  `v3 host success result is invalid JSON: ${err instanceof Error ? err.message : String(err)}`
3697
3834
  );
3698
3835
  }
3699
- if (!isRecord2(raw)) throw new Error("v3 host success result must be an object");
3836
+ if (!isRecord3(raw)) throw new Error("v3 host success result must be an object");
3700
3837
  const allowed = /* @__PURE__ */ new Set([
3701
3838
  "schemaVersion",
3702
3839
  "runId",
@@ -3729,13 +3866,13 @@ function readAndVerifyV3HostSuccessResult(input) {
3729
3866
  ]) {
3730
3867
  if (raw[key] !== input[key]) throw new Error(`v3 host success result ${key} mismatch`);
3731
3868
  }
3732
- if (!isRecord2(raw.externalRefs)) throw new Error("v3 host success result externalRefs must be an object");
3869
+ if (!isRecord3(raw.externalRefs)) throw new Error("v3 host success result externalRefs must be an object");
3733
3870
  canonicalJson(raw.output);
3734
3871
  canonicalJson(raw.externalRefs);
3735
3872
  return raw;
3736
3873
  }
3737
3874
  function parsePreparedHostInput(raw) {
3738
- if (!isRecord2(raw)) throw new Error("v3 host input sidecar must be an object");
3875
+ if (!isRecord3(raw)) throw new Error("v3 host input sidecar must be an object");
3739
3876
  const allowed = /* @__PURE__ */ new Set([
3740
3877
  "schemaVersion",
3741
3878
  "runId",
@@ -3856,12 +3993,12 @@ function assertDirectory(path, label) {
3856
3993
  function sha256(value) {
3857
3994
  return (0, import_node_crypto6.createHash)("sha256").update(value).digest("hex");
3858
3995
  }
3859
- function isRecord2(value) {
3996
+ function isRecord3(value) {
3860
3997
  return typeof value === "object" && value !== null && !Array.isArray(value);
3861
3998
  }
3862
3999
 
3863
4000
  // src/workflows/v3/shared-node-runtime.ts
3864
- function renderGoalFile(goal, resultSchema, loopCtx, nodeInstructions, hasWorkflowParams = false) {
4001
+ function renderGoalFile(goal, resultSchema, loopCtx, nodeInstructions, hasWorkflowParams = false, outputs) {
3865
4002
  const E = GOAL_ENV;
3866
4003
  const kinds = MANIFEST_FILE_KINDS.join(" | ");
3867
4004
  const [okStatus, failStatus] = MANIFEST_STATUSES;
@@ -3879,6 +4016,16 @@ function renderGoalFile(goal, resultSchema, loopCtx, nodeInstructions, hasWorkfl
3879
4016
  `List \`result.json\` in the manifest \`files\` array like any other product (its \`path\` is exactly "result.json"). A missing or schema-violating result.json blocks this node.`,
3880
4017
  ""
3881
4018
  ] : [];
4019
+ const artifactSection = outputs ? [
4020
+ "## Public artifact outputs (REQUIRED for this node)",
4021
+ "These stable output keys are part of the Workflow interface. Write every declared path under the output directory and list it in the success Manifest with the exact kind:",
4022
+ "",
4023
+ " " + JSON.stringify(outputs),
4024
+ "",
4025
+ "Manifest `name` is presentation-only and may be human-readable; downstream nodes resolve these products by output key \u2192 declared path.",
4026
+ "A missing path or mismatched kind blocks the run and requires a Workflow revision; ordinary retry is disabled.",
4027
+ ""
4028
+ ] : [];
3882
4029
  const loopSection = loopCtx ? [
3883
4030
  "## Loop context",
3884
4031
  `This node runs inside loop "${loopCtx.loopId}", iteration ${loopCtx.iteration} of at most ${loopCtx.maxIterations}.`,
@@ -3903,6 +4050,7 @@ function renderGoalFile(goal, resultSchema, loopCtx, nodeInstructions, hasWorkfl
3903
4050
  ...instructionsSection,
3904
4051
  ...paramsSection,
3905
4052
  ...loopSection,
4053
+ ...artifactSection,
3906
4054
  "## How to complete this node",
3907
4055
  "You are an autonomous agent completing exactly ONE botmux v3 workflow node.",
3908
4056
  "Work toward the goal above until it is done, then stop. Do NOT ask the user with interactive tools (they are disabled in this mode). If you genuinely need a human DECISION to proceed, use the human-ask escape hatch described below (also available as the `botmux-goal-ask` skill).",
@@ -3944,6 +4092,8 @@ function classifyTerminal(errorClass, opts) {
3944
4092
  switch (errorClass) {
3945
4093
  case "manifestInvalid":
3946
4094
  // agent wrote a bad manifest — a retry may fix it
4095
+ case "artifactContractInvalid":
4096
+ // definition/manifest public product mismatch
3947
4097
  case "resultInvalid":
3948
4098
  return "blocked";
3949
4099
  case "workerError":
@@ -5448,7 +5598,8 @@ async function runWorkflow(dag, deps, opts) {
5448
5598
  node.resultSchema,
5449
5599
  loopCtx,
5450
5600
  node.override?.systemPromptAppend,
5451
- !!workflowDataPath
5601
+ !!workflowDataPath,
5602
+ node.outputs
5452
5603
  )
5453
5604
  );
5454
5605
  const effSnap = mergeNodeCapability(botSnap, node.override);
@@ -5686,6 +5837,20 @@ async function runWorkflow(dag, deps, opts) {
5686
5837
  return;
5687
5838
  }
5688
5839
  }
5840
+ const artifactContract = validateManifestArtifactContract(node.outputs, verdict.manifest);
5841
+ if (!artifactContract.ok) {
5842
+ appendWorkerOutcome({
5843
+ type: "nodeBlocked",
5844
+ nodeId: node.id,
5845
+ ...instanceId ? { instanceId } : {},
5846
+ attemptId,
5847
+ errorClass: "artifactContractInvalid",
5848
+ errorCode: "OUTPUT_CONTRACT_VIOLATION",
5849
+ recovery: "reviseWorkflow",
5850
+ message: artifactContract.problems.join("; ")
5851
+ });
5852
+ return;
5853
+ }
5689
5854
  appendWorkerOutcome({
5690
5855
  type: "nodeSucceeded",
5691
5856
  nodeId: node.id,
@@ -5941,8 +6106,9 @@ ${request.reason}
5941
6106
  depends: bodyDef.depends.map((d) => ({ from: loopInstanceId(ref.loopId, ref.iteration, d.from) })),
5942
6107
  inputs: bodyDef.inputs.map((r) => ({
5943
6108
  from: loopInstanceId(ref.loopId, ref.iteration, r.from),
5944
- ...r.select ? { select: r.select } : {}
6109
+ ...r.select ? { select: r.select } : {},
5945
6110
  // P3: 实例化时保留 selector
6111
+ ...r.output !== void 0 ? { output: r.output } : {}
5946
6112
  }))
5947
6113
  };
5948
6114
  }
@@ -6142,7 +6308,7 @@ ${request.reason}
6142
6308
  });
6143
6309
  }
6144
6310
  const latestSuccess = (key) => [...events].reverse().find((e) => e.type === "nodeSucceeded" && (e.instanceId ?? e.nodeId) === key);
6145
- const pushFrom = (label, nodeId, filter) => {
6311
+ const pushFrom = (label, nodeId, filter, output) => {
6146
6312
  const key = snap.nodes.get(nodeId)?.effectiveInstanceId ?? nodeId;
6147
6313
  const succ = latestSuccess(key);
6148
6314
  if (!succ) return;
@@ -6152,6 +6318,7 @@ ${request.reason}
6152
6318
  if (filter && !filter(f)) continue;
6153
6319
  inputs.push({
6154
6320
  from: label,
6321
+ ...output ? { output } : {},
6155
6322
  ...succ.instanceId ? { instanceId: succ.instanceId } : {},
6156
6323
  name: f.name,
6157
6324
  path: (0, import_node_path9.join)(upstreamOutputDir, f.path),
@@ -6184,9 +6351,14 @@ ${request.reason}
6184
6351
  };
6185
6352
  const selectorMisses = [];
6186
6353
  const pushRef = (ref) => {
6187
- const filter = ref.select ? (f) => ref.select.name !== void 0 ? f.name === ref.select.name : f.path === ref.select.path : void 0;
6354
+ const source = nodesById.get(ref.from) ?? (loopRef ? nodesById.get(loopRef.loopId).body.nodes.find(
6355
+ (bodyNode) => loopInstanceId(loopRef.loopId, loopRef.iteration, bodyNode.id) === ref.from
6356
+ ) : void 0);
6357
+ const declaredOutputs = source?.type === "loop" ? source.body?.nodes.find((bodyNode) => bodyNode.id === source.output?.from)?.outputs : source?.outputs;
6358
+ const declared = ref.output ? declaredOutputs?.[ref.output] : void 0;
6359
+ const filter = declared ? (f) => f.path === declared.path : ref.select ? (f) => ref.select.name !== void 0 ? f.name === ref.select.name : f.path === ref.select.path : void 0;
6188
6360
  const before = inputs.length;
6189
- pushFrom(ref.from, ref.from, filter);
6361
+ pushFrom(ref.from, ref.from, filter, ref.output);
6190
6362
  if (ref.select && inputs.length === before) {
6191
6363
  selectorMisses.push({ from: ref.from, reason: "selectorMiss" });
6192
6364
  }