chainlesschain 0.162.70 → 0.162.72

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 (171) hide show
  1. package/package.json +4 -3
  2. package/src/assets/web-panel/assets/{AIOps-CnHIXe2L.js → AIOps-1bl50Cen.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-CzDyhTAp.js → ActionButton-Bw4kAx-1.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-rwvDMCd-.js → Analytics-junq7r42.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-D-Q1M-5V.js → AppLayout-vf1TVGeu.js} +5 -5
  6. package/src/assets/web-panel/assets/{Audit-BIFSGnAE.js → Audit-CBzpgcL9.js} +1 -1
  7. package/src/assets/web-panel/assets/{Backup-CbUdiVeS.js → Backup-ChwGPeP4.js} +1 -1
  8. package/src/assets/web-panel/assets/{BaseInput-D3P_pICH.js → BaseInput-2G2KGE6M.js} +1 -1
  9. package/src/assets/web-panel/assets/{Chat-DRrX7vVB.js → Chat-Cr9NwfDA.js} +4 -4
  10. package/src/assets/web-panel/assets/{ChatBubbleRenderer-DTC1ElV-.js → ChatBubbleRenderer-CJvlo_ay.js} +1 -1
  11. package/src/assets/web-panel/assets/{Checkbox-Dpq9X8Xd.js → Checkbox-DN_5PBT7.js} +1 -1
  12. package/src/assets/web-panel/assets/{Codegen-yQQOSuHA.js → Codegen-D4bluAgT.js} +1 -1
  13. package/src/assets/web-panel/assets/{Col-DLFLDxar.js → Col-DBP3UjYW.js} +1 -1
  14. package/src/assets/web-panel/assets/{Community-DYbdX-GB.js → Community-ge-SvR3c.js} +1 -1
  15. package/src/assets/web-panel/assets/{Compact-Dclrp1HE.js → Compact-DGXvlGNv.js} +1 -1
  16. package/src/assets/web-panel/assets/{Compliance-C9EQMgoP.js → Compliance-Cn7nBsFQ.js} +1 -1
  17. package/src/assets/web-panel/assets/{Cowork-B9IMFsMk.js → Cowork-DgHEO2de.js} +2 -2
  18. package/src/assets/web-panel/assets/{Cron-V6TlbDey.js → Cron-DUbYxPs0.js} +2 -2
  19. package/src/assets/web-panel/assets/{Crosschain-Cl_vG8R_.js → Crosschain-3J0MWPQq.js} +1 -1
  20. package/src/assets/web-panel/assets/{DID-DoUUF9cz.js → DID-BR_Jpkh4.js} +2 -2
  21. package/src/assets/web-panel/assets/{Dashboard-DcA8WmrS.js → Dashboard-ffykLkAJ.js} +2 -2
  22. package/src/assets/web-panel/assets/{Dropdown-wyGMYzw9.js → Dropdown-DaI38dXT.js} +1 -1
  23. package/src/assets/web-panel/assets/{EmailListRenderer-B1T3ziIH.js → EmailListRenderer-CnIzkUVG.js} +1 -1
  24. package/src/assets/web-panel/assets/{FamilyGuardDashboard-CW4CwYbZ.js → FamilyGuardDashboard-BxrgAAZg.js} +1 -1
  25. package/src/assets/web-panel/assets/{Federation-9tEiw0KA.js → Federation-BuS_FRh3.js} +1 -1
  26. package/src/assets/web-panel/assets/{FormItemContext-DbocfnNQ.js → FormItemContext-DBG-8g1w.js} +1 -1
  27. package/src/assets/web-panel/assets/{GenericCardRenderer-CGNsG2qI.js → GenericCardRenderer-CmEc8RgH.js} +1 -1
  28. package/src/assets/web-panel/assets/{Git-BxA3dgT8.js → Git-CK10BYwY.js} +2 -2
  29. package/src/assets/web-panel/assets/{Governance-DqnhaC7n.js → Governance-C8cABM_I.js} +1 -1
  30. package/src/assets/web-panel/assets/{Inference-D6Ll4t9c.js → Inference-Dr4_DIw3.js} +1 -1
  31. package/src/assets/web-panel/assets/{KnowledgeGraph-Bb8IXGl7.js → KnowledgeGraph-0_bLuvvb.js} +1 -1
  32. package/src/assets/web-panel/assets/{Logs-DRUUVMdc.js → Logs-PVGZxt9z.js} +2 -2
  33. package/src/assets/web-panel/assets/{Marketplace-BYRM186w.js → Marketplace-BtVnS4HW.js} +1 -1
  34. package/src/assets/web-panel/assets/{McpTools-DLbh7uS8.js → McpTools-BVulusCC.js} +3 -3
  35. package/src/assets/web-panel/assets/{Memory-D2_uZEOI.js → Memory-DEMtNbJ9.js} +2 -2
  36. package/src/assets/web-panel/assets/{MobileBridge-CXVJoMKk.js → MobileBridge-5QL654FQ.js} +3 -3
  37. package/src/assets/web-panel/assets/{MobileProjects-QRZYgIME.js → MobileProjects-G5Du9Thr.js} +1 -1
  38. package/src/assets/web-panel/assets/{Mtc-D3mv30gW.js → Mtc-CNFPJej0.js} +2 -2
  39. package/src/assets/web-panel/assets/{MtcAudit-__QPus22.js → MtcAudit-DAJ81Cej.js} +2 -2
  40. package/src/assets/web-panel/assets/{Multisig-Bu5gaTn6.js → Multisig-Cwj8tXfT.js} +3 -3
  41. package/src/assets/web-panel/assets/{NLProgramming-SjaViyfR.js → NLProgramming-Di0ptaL6.js} +1 -1
  42. package/src/assets/web-panel/assets/{Notes-feXsl0dN.js → Notes-Cse_4Ox6.js} +3 -3
  43. package/src/assets/web-panel/assets/{NotificationSettings-BtVFokbM.js → NotificationSettings-ntpoi5ec.js} +1 -1
  44. package/src/assets/web-panel/assets/OrderTableRenderer-CbKsOM0D.js +1 -0
  45. package/src/assets/web-panel/assets/{Organization-CewL60Rd.js → Organization-Bhjya5h9.js} +4 -4
  46. package/src/assets/web-panel/assets/{Overflow-2ZRChJ7E.js → Overflow-C0u3aive.js} +1 -1
  47. package/src/assets/web-panel/assets/{P2P-DuMr6G9y.js → P2P-BWrTwf_a.js} +2 -2
  48. package/src/assets/web-panel/assets/{PdhVaultBrowser-BMOcHCWd.js → PdhVaultBrowser-pld2Qlqk.js} +3 -3
  49. package/src/assets/web-panel/assets/{Permissions-Cj94ql75.js → Permissions-CW5FG3mE.js} +4 -4
  50. package/src/assets/web-panel/assets/{PersonalDataHub-BYK4mlk7.js → PersonalDataHub-BuBmqfDa.js} +4 -4
  51. package/src/assets/web-panel/assets/{Pipeline-maRvdrbO.js → Pipeline-nZr06BNO.js} +1 -1
  52. package/src/assets/web-panel/assets/{Privacy-CKb2zZJz.js → Privacy-B_XvMHYv.js} +1 -1
  53. package/src/assets/web-panel/assets/{ProjectInit-C2pfXv4l.js → ProjectInit-CbRHJy85.js} +2 -2
  54. package/src/assets/web-panel/assets/{ProjectSettings-DGeNwq0h.js → ProjectSettings-CCH8SpEa.js} +2 -2
  55. package/src/assets/web-panel/assets/{Projects-BRypTQUV.js → Projects-D5Min2Fu.js} +1 -1
  56. package/src/assets/web-panel/assets/{Providers-DYwMPXEO.js → Providers-DgCY5A8A.js} +1 -1
  57. package/src/assets/web-panel/assets/{QuickAsk-DE6isYZV.js → QuickAsk-DEcKTiTV.js} +1 -1
  58. package/src/assets/web-panel/assets/{Recommend-Rc-AE6y9.js → Recommend-BRaq2oKc.js} +1 -1
  59. package/src/assets/web-panel/assets/{Reputation-DDQ8_kmM.js → Reputation-DzdbBdVv.js} +1 -1
  60. package/src/assets/web-panel/assets/{Row-Do5iQUFj.js → Row-Ds2JNh7c.js} +1 -1
  61. package/src/assets/web-panel/assets/{RssFeed-cxG0ulSw.js → RssFeed-Bk1-t4Ie.js} +3 -3
  62. package/src/assets/web-panel/assets/{Search-D3-8EdNk.js → Search-DaVhRwoq.js} +1 -1
  63. package/src/assets/web-panel/assets/{Security-DtBfws1D.js → Security-D9mXjPSu.js} +3 -3
  64. package/src/assets/web-panel/assets/{Services-DzeN8HsG.js → Services-D8sukL97.js} +2 -2
  65. package/src/assets/web-panel/assets/{Skeleton-BQCVeTUf.js → Skeleton-COCJt30X.js} +1 -1
  66. package/src/assets/web-panel/assets/Skills-DtFPSPSc.js +1 -0
  67. package/src/assets/web-panel/assets/{Sla-d2IOd-AA.js → Sla-DIab-wjR.js} +1 -1
  68. package/src/assets/web-panel/assets/{SpeechSettings-BjfopwT6.js → SpeechSettings-xxIH9hgB.js} +1 -1
  69. package/src/assets/web-panel/assets/{SyncSettings-D5_c1_rI.js → SyncSettings-DrpxzJOH.js} +2 -2
  70. package/src/assets/web-panel/assets/Tasks-maiJ3jAT.js +1 -0
  71. package/src/assets/web-panel/assets/{Templates-CCXFgNy3.js → Templates-COE-dSks.js} +1 -1
  72. package/src/assets/web-panel/assets/{Tenant-Duz6eEK3.js → Tenant-CLGObu4M.js} +1 -1
  73. package/src/assets/web-panel/assets/{Terminal-Buc6UbYo.js → Terminal-Bw8WBvW3.js} +2 -2
  74. package/src/assets/web-panel/assets/{TimelineRenderer-Du5jKHJz.js → TimelineRenderer-D4gdk3Qb.js} +1 -1
  75. package/src/assets/web-panel/assets/{Tokens-B12vLtQm.js → Tokens-CSoL_uAw.js} +1 -1
  76. package/src/assets/web-panel/assets/{Trigger-s_pHOerx.js → Trigger-BCRcowFi.js} +1 -1
  77. package/src/assets/web-panel/assets/{Trust-4sDhbAls.js → Trust-D5QPZ198.js} +1 -1
  78. package/src/assets/web-panel/assets/{UkeySign-UeyFknip.js → UkeySign-C7SsMqfZ.js} +1 -1
  79. package/src/assets/web-panel/assets/{VideoEditing-DsopM2S5.js → VideoEditing-CDQ4HNZL.js} +1 -1
  80. package/src/assets/web-panel/assets/{Wallet-CreLjC5U.js → Wallet-VmG4P_Fm.js} +3 -3
  81. package/src/assets/web-panel/assets/{WebAuthn-CBuMMZbK.js → WebAuthn-qtC0dZxA.js} +4 -4
  82. package/src/assets/web-panel/assets/{WorkflowEditor-DWhHHSFW.js → WorkflowEditor-C_CSdk3l.js} +1 -1
  83. package/src/assets/web-panel/assets/{chat-Iurq_BMY.js → chat-BGm8YCIo.js} +1 -1
  84. package/src/assets/web-panel/assets/{colors-BIOTIa89.js → colors-BScOmAwk.js} +1 -1
  85. package/src/assets/web-panel/assets/{compact-item-B7lPu9pO.js → compact-item-HDwaT31n.js} +1 -1
  86. package/src/assets/web-panel/assets/{createContext-Bzzk7mrh.js → createContext-CheSjPm8.js} +1 -1
  87. package/src/assets/web-panel/assets/devWarning-CjO6o19j.js +1 -0
  88. package/src/assets/web-panel/assets/{hasIn-B11xqp5O.js → hasIn-C1_NbX0O.js} +1 -1
  89. package/src/assets/web-panel/assets/index--jhpSxS1.js +1 -0
  90. package/src/assets/web-panel/assets/{index-vZCGFSJN.js → index-6C8mxO8j.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-CLhpFnSR.js → index-B94_DZF-.js} +1 -1
  92. package/src/assets/web-panel/assets/{index-BlR8YVlt.js → index-BGROuzQG.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-CxDOX4g6.js → index-BKQordyU.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-DMrzHgE6.js → index-BMxPL1oG.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-DsoyfOzW.js → index-Bd2zS3jK.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-3LgeP4ij.js → index-ByoUjX8f.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-Y4gmywZo.js → index-C4FSaMAu.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-CCxGEK8b.js → index-C6WXnaG4.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-C-lwsB2F.js → index-C71Z__li.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-C4wV32Dn.js → index-CSaztnyZ.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-CiXNVmcx.js → index-CYkezxqK.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-D6q-fkXs.js → index-Clqj2JTi.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-R-YSpvg4.js → index-Cm55PcYG.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-Dn_Ht3v2.js → index-CxoOEb-l.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-DEKeoJ6I.js → index-D2gy4IX0.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-AmHa_sOB.js → index-D8xpncJ0.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-CULCwI_H.js → index-DB6gbgU2.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-tub9w6us.js → index-DDk0s3nO.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-CzY2mdiR.js → index-DQ01dBAS.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-5BkUKVyj.js → index-DSuWcpsv.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-D8tm9JnQ.js → index-DVyL9jbT.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-BLREN4NE.js → index-DaATJtzu.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-CEKRqrI-.js → index-DbfAaBxL.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-BVIXYV1R.js → index-DhTELUKN.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-BgmBZgU9.js → index-Dk2N6hxE.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-B4rXFIOG.js → index-DkiIxTIS.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-CA-GPOd9.js → index-DoZwBGUX.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-6QGNBg3m.js → index-DppSbT8L.js} +3 -3
  119. package/src/assets/web-panel/assets/{index-DwGVPCZb.js → index-DzE-95XH.js} +1 -1
  120. package/src/assets/web-panel/assets/index-J4Rzclyc.js +1 -0
  121. package/src/assets/web-panel/assets/{index-BIFscFlm.js → index-KG9xVANC.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-DyV9331f.js → index-KXrUKw1h.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-D25-u3Q2.js → index-eSBoP6E_.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-B93pM-xx.js → index-ggl9cj35.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-htxy7qDG.js → index-i-xYmSsZ.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-CS2UWrIt.js → index-lB5kN9Yc.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-DX7HGwP_.js → index-yKmudyK7.js} +1 -1
  128. package/src/assets/web-panel/assets/{initDefaultProps-Bl6klCzd.js → initDefaultProps-D7Q7zDzP.js} +1 -1
  129. package/src/assets/web-panel/assets/{motion-B9IxS1-S.js → motion-B4DcaqPb.js} +1 -1
  130. package/src/assets/web-panel/assets/{move-NJ5XDNQe.js → move-CYxmYrFY.js} +1 -1
  131. package/src/assets/web-panel/assets/{omit-ByO8GI_H.js → omit-ze_9dKw3.js} +1 -1
  132. package/src/assets/web-panel/assets/{pickAttrs-W6iTcLuH.js → pickAttrs-D24HrRSv.js} +1 -1
  133. package/src/assets/web-panel/assets/{placementArrow-JMndCFEP.js → placementArrow-BWJZVYyS.js} +1 -1
  134. package/src/assets/web-panel/assets/{responsiveObserve-DHV7-FuY.js → responsiveObserve-CseJia_L.js} +1 -1
  135. package/src/assets/web-panel/assets/{slide-DioRUeNF.js → slide-CBWNEpey.js} +1 -1
  136. package/src/assets/web-panel/assets/{statusUtils-DMp2b9Ze.js → statusUtils-f4e6p7yd.js} +1 -1
  137. package/src/assets/web-panel/assets/{styleChecker-DamP5BTS.js → styleChecker-DZmfWZ8G.js} +1 -1
  138. package/src/assets/web-panel/assets/{useFlexGapSupport-D338iARz.js → useFlexGapSupport-4RBaBnVI.js} +1 -1
  139. package/src/assets/web-panel/assets/{useFs-DE_HJgk6.js → useFs-Cv6bNoEA.js} +1 -1
  140. package/src/assets/web-panel/assets/{usePersonalDataHub-914YxO9q.js → usePersonalDataHub-DOTETJIA.js} +1 -1
  141. package/src/assets/web-panel/assets/{vnode-CY39Ky7P.js → vnode-DqS1K6-J.js} +1 -1
  142. package/src/assets/web-panel/assets/{zoom-BEghlCth.js → zoom-C1MYzIJG.js} +1 -1
  143. package/src/assets/web-panel/index.html +1 -1
  144. package/src/commands/cowork.js +8 -0
  145. package/src/commands/crosschain.js +32 -4
  146. package/src/commands/init.js +10 -10
  147. package/src/commands/loop.js +9 -3
  148. package/src/commands/memory.js +6 -4
  149. package/src/commands/orchestrate.js +5 -2
  150. package/src/commands/video.js +5 -1
  151. package/src/lib/cowork-workflow.js +664 -129
  152. package/src/lib/hook-manager.js +3 -2
  153. package/src/lib/micro-compact.js +52 -0
  154. package/src/lib/permission-rules.cjs +39 -0
  155. package/src/lib/skill-loader.js +62 -43
  156. package/src/lib/workflow-state-reader.js +10 -1
  157. package/src/repl/agent-repl.js +228 -20
  158. package/src/repl/chat-repl.js +4 -2
  159. package/src/repl/permission-tier.js +60 -0
  160. package/src/repl/stream-decision.js +16 -0
  161. package/src/repl/think-command.js +36 -0
  162. package/src/runtime/agent-core.js +42 -6
  163. package/src/runtime/file-ref-expander.js +209 -18
  164. package/src/runtime/headless-runner.js +3 -3
  165. package/src/runtime/headless-stream.js +16 -3
  166. package/src/assets/web-panel/assets/OrderTableRenderer-CJJ2GWDf.js +0 -1
  167. package/src/assets/web-panel/assets/Skills-D6mCVKt7.js +0 -1
  168. package/src/assets/web-panel/assets/Tasks-BCmxDMZx.js +0 -1
  169. package/src/assets/web-panel/assets/devWarning-CuIq7zBB.js +0 -1
  170. package/src/assets/web-panel/assets/index-BRsns-SP.js +0 -1
  171. package/src/assets/web-panel/assets/index-QTuoOvJA.js +0 -1
@@ -36,6 +36,9 @@ import { evaluate as evalExpr, resolveReference } from "./workflow-expr.js";
36
36
  /** Maximum number of items a single forEach step can expand into. */
37
37
  export const MAX_FAN_OUT = 500;
38
38
 
39
+ /** Absolute ceiling on a single loop step's iterations (infinite-loop guard). */
40
+ export const MAX_LOOP_ITERATIONS = 100;
41
+
39
42
  export const _deps = {
40
43
  existsSync,
41
44
  mkdirSync,
@@ -46,6 +49,10 @@ export const _deps = {
46
49
  appendFileSync,
47
50
  now: () => Date.now(),
48
51
  runTask: null, // injected by CLI
52
+ // Timer seams (injectable for deterministic retry/timeout tests).
53
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
54
+ setTimeout: (fn, ms) => setTimeout(fn, ms),
55
+ clearTimeout: (t) => clearTimeout(t),
49
56
  };
50
57
 
51
58
  // ─── Paths ───────────────────────────────────────────────────────────────────
@@ -104,6 +111,61 @@ export function validateWorkflow(wf) {
104
111
  );
105
112
  }
106
113
  }
114
+ if (
115
+ s.retries !== undefined &&
116
+ (typeof s.retries !== "number" ||
117
+ !Number.isInteger(s.retries) ||
118
+ s.retries < 0)
119
+ ) {
120
+ errors.push(`steps[${i}].retries must be a non-negative integer`);
121
+ }
122
+ if (
123
+ s.timeoutMs !== undefined &&
124
+ (typeof s.timeoutMs !== "number" ||
125
+ !Number.isFinite(s.timeoutMs) ||
126
+ s.timeoutMs <= 0)
127
+ ) {
128
+ errors.push(`steps[${i}].timeoutMs must be a positive number`);
129
+ }
130
+ if (
131
+ s.retryDelayMs !== undefined &&
132
+ (typeof s.retryDelayMs !== "number" ||
133
+ !Number.isFinite(s.retryDelayMs) ||
134
+ s.retryDelayMs < 0)
135
+ ) {
136
+ errors.push(`steps[${i}].retryDelayMs must be a non-negative number`);
137
+ }
138
+ if (
139
+ s.retryBackoff !== undefined &&
140
+ s.retryBackoff !== "fixed" &&
141
+ s.retryBackoff !== "exponential"
142
+ ) {
143
+ errors.push(
144
+ `steps[${i}].retryBackoff must be "fixed" or "exponential"`,
145
+ );
146
+ }
147
+ const hasWhile = s.loopWhile !== undefined;
148
+ const hasUntil = s.loopUntil !== undefined;
149
+ if (hasWhile && typeof s.loopWhile !== "string") {
150
+ errors.push(`steps[${i}].loopWhile must be a string expression`);
151
+ }
152
+ if (hasUntil && typeof s.loopUntil !== "string") {
153
+ errors.push(`steps[${i}].loopUntil must be a string expression`);
154
+ }
155
+ if (hasWhile && hasUntil) {
156
+ errors.push(`steps[${i}] cannot set both loopWhile and loopUntil`);
157
+ }
158
+ if ((hasWhile || hasUntil) && s.forEach !== undefined) {
159
+ errors.push(`steps[${i}] cannot combine a loop with forEach`);
160
+ }
161
+ if (
162
+ s.maxIterations !== undefined &&
163
+ (typeof s.maxIterations !== "number" ||
164
+ !Number.isInteger(s.maxIterations) ||
165
+ s.maxIterations <= 0)
166
+ ) {
167
+ errors.push(`steps[${i}].maxIterations must be a positive integer`);
168
+ }
107
169
  }
108
170
  // Check dependsOn references exist
109
171
  for (const s of wf.steps) {
@@ -329,17 +391,467 @@ export function removeWorkflow(cwd, id) {
329
391
  return true;
330
392
  }
331
393
 
394
+ // ─── Per-step retry / timeout ─────────────────────────────────────────────────
395
+
396
+ /**
397
+ * Compute the delay (ms) to wait BEFORE a retry, given the just-failed attempt
398
+ * number (1-based). `fixed` returns `retryDelayMs` verbatim; `exponential`
399
+ * doubles it per prior attempt (delay = base · 2^(attempt-1)).
400
+ */
401
+ export function retryDelayFor(step, attempt) {
402
+ const base = Number(step.retryDelayMs) || 0;
403
+ if (base <= 0) return 0;
404
+ if (step.retryBackoff === "exponential") {
405
+ return base * Math.pow(2, attempt - 1);
406
+ }
407
+ return base;
408
+ }
409
+
410
+ /**
411
+ * Race a promise (produced by `factory`) against a per-attempt timeout.
412
+ * Resolves/rejects with the promise when it settles first; rejects with a
413
+ * "timed out" error if the timer fires first. The timer is always cleared, and
414
+ * a late rejection from the losing promise is swallowed to avoid an unhandled
415
+ * rejection (the underlying task is best-effort abandoned — runTask has no
416
+ * cancellation contract).
417
+ */
418
+ export async function withTimeout(factory, timeoutMs) {
419
+ if (!timeoutMs || timeoutMs <= 0) return factory();
420
+ const p = Promise.resolve().then(factory);
421
+ let timer = null;
422
+ const timeout = new Promise((_resolve, reject) => {
423
+ timer = _deps.setTimeout(() => {
424
+ reject(new Error(`step timed out after ${timeoutMs}ms`));
425
+ }, timeoutMs);
426
+ });
427
+ try {
428
+ return await Promise.race([p, timeout]);
429
+ } finally {
430
+ if (timer != null) _deps.clearTimeout(timer);
431
+ p.catch(() => {}); // guard a late rejection from the abandoned task
432
+ }
433
+ }
434
+
435
+ /**
436
+ * Run one step's task with optional `timeoutMs` and `retries` (with `fixed` or
437
+ * `exponential` `retryDelayMs` backoff). An attempt counts as a failure if the
438
+ * task throws, times out, or returns a non-`completed` status. Returns
439
+ * `{ ok, entry?, error?, attempts }`. `attempts` is the total number of tries.
440
+ */
441
+ export async function runStepWithRetry({ step, message, cwd, llmOptions }) {
442
+ const maxRetries = Math.max(0, Math.floor(Number(step.retries) || 0));
443
+ const timeoutMs = Number(step.timeoutMs) || 0;
444
+ let lastEntry = null;
445
+ let lastErr = null;
446
+ for (let attempt = 1; attempt <= maxRetries + 1; attempt++) {
447
+ try {
448
+ const entry = await withTimeout(
449
+ () =>
450
+ _deps.runTask({
451
+ templateId: step.templateId || null,
452
+ userMessage: message,
453
+ files: step.files || [],
454
+ cwd,
455
+ llmOptions,
456
+ }),
457
+ timeoutMs,
458
+ );
459
+ if (entry && entry.status === "completed") {
460
+ return { ok: true, entry, attempts: attempt };
461
+ }
462
+ lastEntry = entry; // ran but not completed → retry-eligible
463
+ lastErr = null;
464
+ } catch (err) {
465
+ lastErr = err; // threw or timed out → retry-eligible
466
+ lastEntry = null;
467
+ }
468
+ if (attempt <= maxRetries) {
469
+ const delay = retryDelayFor(step, attempt);
470
+ if (delay > 0) await _deps.sleep(delay);
471
+ }
472
+ }
473
+ const attempts = maxRetries + 1;
474
+ if (lastEntry) return { ok: false, entry: lastEntry, attempts };
475
+ return { ok: false, error: lastErr, attempts };
476
+ }
477
+
478
+ /** Attach an `attempts` field only when more than one try occurred (keeps the
479
+ * single-attempt result shape byte-identical to the pre-retry behavior). */
480
+ function _withAttempts(result, attempts) {
481
+ if (attempts > 1) return { ...(result || {}), attempts };
482
+ return result;
483
+ }
484
+
485
+ /** Build a single-step outcome object from a `runStepWithRetry` result. */
486
+ function outcomeFromRetry(recordId, r) {
487
+ if (r.ok || r.entry) {
488
+ return {
489
+ id: recordId,
490
+ status: r.entry.status,
491
+ taskId: r.entry.taskId,
492
+ result: _withAttempts(r.entry.result, r.attempts),
493
+ };
494
+ }
495
+ return {
496
+ id: recordId,
497
+ status: "failed",
498
+ taskId: null,
499
+ result: _withAttempts(
500
+ { summary: `Step threw: ${r.error.message}` },
501
+ r.attempts,
502
+ ),
503
+ };
504
+ }
505
+
506
+ // ─── while / until loop nodes ──────────────────────────────────────────────────
507
+
508
+ /** True when a step is a loop node (`loopWhile` or `loopUntil`). */
509
+ export function isLoopStep(step) {
510
+ return step.loopWhile !== undefined || step.loopUntil !== undefined;
511
+ }
512
+
513
+ /** Resolve a loop step's iteration cap, clamped to MAX_LOOP_ITERATIONS. */
514
+ export function loopIterationCap(step) {
515
+ const m = Number(step.maxIterations);
516
+ if (Number.isFinite(m) && m > 0) {
517
+ return Math.min(Math.floor(m), MAX_LOOP_ITERATIONS);
518
+ }
519
+ return MAX_LOOP_ITERATIONS;
520
+ }
521
+
522
+ /**
523
+ * Substitute loop-local tokens in a message template: `${iter}` → the 1-based
524
+ * iteration number, `${self.<field>}` → the step's own most-recent iteration
525
+ * result (empty on the first iteration). Other `${step.<id>.<field>}` tokens
526
+ * are left for `substitutePlaceholders`.
527
+ */
528
+ export function substituteLoopVars(template, { stepId, iter, resultsById }) {
529
+ if (typeof template !== "string") return template;
530
+ let out = template.replace(/\$\{iter\}/g, String(iter));
531
+ out = out.replace(/\$\{self\.([\w-]+)\}/g, (_, field) => {
532
+ const entry = resultsById?.get?.(stepId);
533
+ if (!entry) return "";
534
+ if (field === "summary") return entry.result?.summary ?? "";
535
+ if (field === "status") return entry.status ?? "";
536
+ if (field === "taskId") return entry.taskId ?? "";
537
+ if (field === "iterations") return String(entry.result?.iterations ?? 0);
538
+ if (field === "tokenCount") return String(entry.result?.tokenCount ?? 0);
539
+ const v = entry.result?.[field];
540
+ return v == null ? "" : String(v);
541
+ });
542
+ return out;
543
+ }
544
+
545
+ /**
546
+ * Evaluate whether a loop step should run another iteration (post-test). The
547
+ * condition may reference `${self.<field>}` (the just-stored iteration result)
548
+ * and `${iter}`. `loopWhile` continues while the expression is true; `loopUntil`
549
+ * continues until it becomes true. Throws on a malformed expression.
550
+ */
551
+ export function evalLoopContinue(step, { stepId, iter, resultsById }) {
552
+ const isWhile = step.loopWhile !== undefined;
553
+ const expr = isWhile ? step.loopWhile : step.loopUntil;
554
+ const subst = String(expr)
555
+ .replace(/\$\{iter\}/g, String(iter))
556
+ .replace(/\$\{self\.([\w-]+)\}/g, (_, f) => `\${step.${stepId}.${f}}`);
557
+ const val = evalExpr(subst, { step: resultsById });
558
+ return isWhile ? val === true : val !== true;
559
+ }
560
+
561
+ /**
562
+ * Run a loop step: repeat its task until the `loopWhile`/`loopUntil` condition
563
+ * says to stop, a failing iteration aborts it, or the iteration cap is hit.
564
+ * Each iteration inherits the step's retry/timeout config. The final result
565
+ * carries `iterations`, `loopExhausted`, and `loopStop` (`condition`|`cap`|
566
+ * `failed`|`bad-condition`).
567
+ */
568
+ export async function runLoopStep({
569
+ step,
570
+ recordId,
571
+ cwd,
572
+ llmOptions,
573
+ resultsById,
574
+ }) {
575
+ const cap = loopIterationCap(step);
576
+ let last = null;
577
+ let iterations = 0;
578
+ let stopReason = "cap";
579
+ for (let iter = 1; iter <= cap; iter++) {
580
+ iterations = iter;
581
+ const withSelf = substituteLoopVars(step.message, {
582
+ stepId: recordId,
583
+ iter,
584
+ resultsById,
585
+ });
586
+ const message = substitutePlaceholders(withSelf, resultsById);
587
+ const r = await runStepWithRetry({ step, message, cwd, llmOptions });
588
+ last = outcomeFromRetry(recordId, r);
589
+ resultsById.set(recordId, last);
590
+ if (last.status === "failed") {
591
+ stopReason = "failed";
592
+ break;
593
+ }
594
+ let cont;
595
+ try {
596
+ cont = evalLoopContinue(step, { stepId: recordId, iter, resultsById });
597
+ } catch (err) {
598
+ last = {
599
+ id: recordId,
600
+ status: "failed",
601
+ taskId: null,
602
+ result: {
603
+ summary: `invalid loop condition on '${recordId}': ${err.message}`,
604
+ },
605
+ };
606
+ resultsById.set(recordId, last);
607
+ stopReason = "bad-condition";
608
+ break;
609
+ }
610
+ if (!cont) {
611
+ stopReason = "condition";
612
+ break;
613
+ }
614
+ }
615
+ return {
616
+ id: recordId,
617
+ status: last ? last.status : "skipped",
618
+ taskId: last?.taskId ?? null,
619
+ result: {
620
+ ...(last?.result || {}),
621
+ iterations,
622
+ loopExhausted: stopReason === "cap",
623
+ loopStop: stopReason,
624
+ },
625
+ };
626
+ }
627
+
628
+ // ─── Step node + no-barrier pipeline ──────────────────────────────────────────
629
+
630
+ /**
631
+ * Run a single step to completion, resolving its when-gate / loop / forEach /
632
+ * plain task exactly as the batch executor does, and storing results (including
633
+ * a forEach parent aggregate) in `resultsById`. Returns `{ outcomes, failed }`:
634
+ * `outcomes` are the entries to add to the run's step list (forEach contributes
635
+ * its children, not the parent); `failed` is true when any non-skipped outcome
636
+ * is not "completed". Used by the no-barrier pipeline scheduler.
637
+ */
638
+ export async function runStepNode(step, ctx) {
639
+ const { resultsById, cwd, llmOptions, onStepStart, onStepComplete } = ctx;
640
+ const recordId = step.id;
641
+ const single = (status, summary) => {
642
+ const o = { id: recordId, status, taskId: null, result: { summary } };
643
+ resultsById.set(recordId, o);
644
+ return {
645
+ outcomes: [o],
646
+ failed: status !== "completed" && status !== "skipped",
647
+ };
648
+ };
649
+
650
+ let runThis;
651
+ try {
652
+ runThis = shouldRunStep(step, resultsById);
653
+ } catch (err) {
654
+ return single("failed", err.message);
655
+ }
656
+ if (!runThis) return single("skipped", "when-condition false");
657
+
658
+ if (isLoopStep(step)) {
659
+ if (onStepStart) onStepStart({ stepId: recordId, message: step.message });
660
+ const o = await runLoopStep({
661
+ step,
662
+ recordId,
663
+ cwd,
664
+ llmOptions,
665
+ resultsById,
666
+ });
667
+ resultsById.set(recordId, o);
668
+ if (onStepComplete) onStepComplete(o);
669
+ return {
670
+ outcomes: [o],
671
+ failed: o.status !== "completed" && o.status !== "skipped",
672
+ };
673
+ }
674
+
675
+ if (step.forEach !== undefined) {
676
+ let items;
677
+ try {
678
+ items = resolveForEachItems(step.forEach, resultsById);
679
+ } catch (err) {
680
+ return single("failed", err.message);
681
+ }
682
+ if (items.length === 0) return single("skipped", "forEach items empty");
683
+ const children = await Promise.all(
684
+ items.map(async (item, k) => {
685
+ const childId = `${recordId}[${k}]`;
686
+ const withItem = substituteItem(step.message, item);
687
+ const msg = substitutePlaceholders(withItem, resultsById);
688
+ if (onStepStart) onStepStart({ stepId: childId, message: msg });
689
+ const r = await runStepWithRetry({
690
+ step,
691
+ message: msg,
692
+ cwd,
693
+ llmOptions,
694
+ });
695
+ const co = outcomeFromRetry(childId, r);
696
+ resultsById.set(childId, co);
697
+ if (onStepComplete) onStepComplete(co);
698
+ return co;
699
+ }),
700
+ );
701
+ const allOk = children.every((c) => c.status === "completed");
702
+ const anyOk = children.some((c) => c.status === "completed");
703
+ resultsById.set(recordId, {
704
+ id: recordId,
705
+ status: allOk ? "completed" : anyOk ? "partial" : "failed",
706
+ taskId: null,
707
+ result: {
708
+ summary: children.map((c) => c.result?.summary ?? "").join("\n"),
709
+ children: children.length,
710
+ },
711
+ });
712
+ return {
713
+ outcomes: children,
714
+ failed: children.some((c) => c.status !== "completed"),
715
+ };
716
+ }
717
+
718
+ const message = substitutePlaceholders(step.message, resultsById);
719
+ if (onStepStart) onStepStart({ stepId: recordId, message });
720
+ const r = await runStepWithRetry({ step, message, cwd, llmOptions });
721
+ const o = outcomeFromRetry(recordId, r);
722
+ resultsById.set(recordId, o);
723
+ if (onStepComplete) onStepComplete(o);
724
+ return { outcomes: [o], failed: o.status !== "completed" };
725
+ }
726
+
727
+ /**
728
+ * No-barrier pipeline scheduler: start each step the instant *its own*
729
+ * dependencies finish, rather than waiting for the whole dependency level.
730
+ * Up to `maxParallel` step nodes run concurrently. On failure with
731
+ * `continueOnError` off, no new steps are scheduled (in-flight ones finish) and
732
+ * the rest are marked skipped. Produces the same outcome set as the batch
733
+ * executor — only the wall-clock idle between levels is removed.
734
+ */
735
+ export async function runPipeline({
736
+ steps,
737
+ resultsById,
738
+ maxParallel = 4,
739
+ continueOnError = false,
740
+ cwd,
741
+ llmOptions = {},
742
+ onStepStart,
743
+ onStepComplete,
744
+ }) {
745
+ const limit = Math.max(1, Math.floor(maxParallel) || 1);
746
+ const remainingDeps = new Map(
747
+ steps.map((s) => [s.id, new Set(s.dependsOn || [])]),
748
+ );
749
+ const dependents = new Map(steps.map((s) => [s.id, []]));
750
+ for (const s of steps) {
751
+ for (const d of s.dependsOn || []) {
752
+ if (dependents.has(d)) dependents.get(d).push(s.id);
753
+ }
754
+ }
755
+ const scheduled = new Set();
756
+ const stepOutcomes = [];
757
+ let anyFailure = false;
758
+ let halted = false;
759
+ let active = 0;
760
+
761
+ await new Promise((resolve) => {
762
+ function pump() {
763
+ if (!halted) {
764
+ for (const s of steps) {
765
+ if (active >= limit) break;
766
+ if (scheduled.has(s.id)) continue;
767
+ if (remainingDeps.get(s.id).size !== 0) continue;
768
+ scheduled.add(s.id);
769
+ active++;
770
+ launch(s);
771
+ }
772
+ }
773
+ if (active === 0 && (halted || scheduled.size === steps.length)) {
774
+ for (const s of steps) {
775
+ if (scheduled.has(s.id)) continue;
776
+ const o = {
777
+ id: s.id,
778
+ status: "skipped",
779
+ taskId: null,
780
+ result: { summary: "skipped due to earlier failure" },
781
+ };
782
+ resultsById.set(s.id, o);
783
+ stepOutcomes.push(o);
784
+ scheduled.add(s.id);
785
+ }
786
+ resolve();
787
+ }
788
+ }
789
+
790
+ async function launch(step) {
791
+ let res;
792
+ try {
793
+ res = await runStepNode(step, {
794
+ resultsById,
795
+ cwd,
796
+ llmOptions,
797
+ onStepStart,
798
+ onStepComplete,
799
+ });
800
+ } catch (err) {
801
+ const o = {
802
+ id: step.id,
803
+ status: "failed",
804
+ taskId: null,
805
+ result: { summary: `Step threw: ${err.message}` },
806
+ };
807
+ resultsById.set(step.id, o);
808
+ res = { outcomes: [o], failed: true };
809
+ }
810
+ stepOutcomes.push(...res.outcomes);
811
+ active--;
812
+ if (res.failed) {
813
+ anyFailure = true;
814
+ if (!continueOnError) halted = true;
815
+ }
816
+ for (const depId of dependents.get(step.id) || []) {
817
+ remainingDeps.get(depId).delete(step.id);
818
+ }
819
+ pump();
820
+ }
821
+
822
+ pump();
823
+ });
824
+
825
+ return { stepOutcomes, anyFailure };
826
+ }
827
+
332
828
  // ─── Execution ───────────────────────────────────────────────────────────────
333
829
 
334
830
  /**
335
831
  * Execute a workflow. The runner for individual tasks must be injected via
336
832
  * `_deps.runTask` (signature matches `runCoworkTask`).
337
833
  *
834
+ * Per-step robustness fields (all optional): `retries` (extra attempts after
835
+ * the first; non-negative integer), `timeoutMs` (per-attempt timeout, positive
836
+ * number), `retryDelayMs` (base delay before each retry, non-negative number),
837
+ * `retryBackoff` (`"fixed"` (default) or `"exponential"`).
838
+ *
839
+ * Loop nodes (optional, mutually exclusive with `forEach`): `loopWhile` /
840
+ * `loopUntil` (a `workflow-expr` condition that may reference `${self.<field>}`
841
+ * — the step's own latest iteration result — and `${iter}`) repeat the step's
842
+ * task (post-test) until the condition stops it, an iteration fails, or
843
+ * `maxIterations` (≤ MAX_LOOP_ITERATIONS) is hit. Each iteration inherits the
844
+ * step's retry/timeout config.
845
+ *
338
846
  * @param {object} options
339
847
  * @param {object} options.workflow - Workflow definition
340
848
  * @param {string} [options.cwd] - Working directory for history
341
- * @param {number} [options.maxParallel] - Max parallel steps per batch
849
+ * @param {number} [options.maxParallel] - Max parallel steps (per batch, or
850
+ * concurrent step nodes in pipeline mode)
342
851
  * @param {boolean} [options.continueOnError] - Keep running after a failure
852
+ * @param {boolean} [options.pipeline] - No-barrier scheduling: start each step
853
+ * as soon as its own deps finish instead of waiting for the dependency level.
854
+ * Defaults to `workflow.pipeline ?? false`. Same outcomes, less idle wait.
343
855
  * @param {object} [options.llmOptions] - Forwarded to each task
344
856
  * @param {function} [options.onStepStart]
345
857
  * @param {function} [options.onStepComplete]
@@ -360,6 +872,7 @@ export async function executeWorkflow(options = {}) {
360
872
  llmOptions = {},
361
873
  onStepStart,
362
874
  onStepComplete,
875
+ pipeline,
363
876
  } = options;
364
877
 
365
878
  const { valid, errors } = validateWorkflow(workflow);
@@ -370,67 +883,54 @@ export async function executeWorkflow(options = {}) {
370
883
  );
371
884
  }
372
885
 
373
- const batches = planBatches(workflow.steps);
886
+ const usePipeline = pipeline ?? workflow.pipeline ?? false;
374
887
  const resultsById = new Map();
375
- const stepOutcomes = [];
376
888
  const startedAt = new Date(_deps.now()).toISOString();
377
- let anyFailure = false;
889
+ let stepOutcomes;
890
+ let anyFailure;
378
891
 
379
- for (const batch of batches) {
380
- // Respect maxParallel by slicing batch into chunks
381
- const chunks = [];
382
- for (let i = 0; i < batch.length; i += maxParallel) {
383
- chunks.push(batch.slice(i, i + maxParallel));
384
- }
892
+ if (usePipeline) {
893
+ ({ stepOutcomes, anyFailure } = await runPipeline({
894
+ steps: workflow.steps,
895
+ resultsById,
896
+ maxParallel,
897
+ continueOnError,
898
+ cwd,
899
+ llmOptions,
900
+ onStepStart,
901
+ onStepComplete,
902
+ }));
903
+ } else {
904
+ stepOutcomes = [];
905
+ anyFailure = false;
906
+ const batches = planBatches(workflow.steps);
907
+ for (const batch of batches) {
908
+ // Respect maxParallel by slicing batch into chunks
909
+ const chunks = [];
910
+ for (let i = 0; i < batch.length; i += maxParallel) {
911
+ chunks.push(batch.slice(i, i + maxParallel));
912
+ }
385
913
 
386
- for (const chunk of chunks) {
387
- // Expand forEach / when into concrete tasks for this chunk
388
- const runnable = []; // { step, message, recordId, parentId }
389
- const preOutcomes = []; // outcomes produced synchronously (skipped)
390
- for (const step of chunk) {
391
- if (anyFailure && !continueOnError) {
392
- const outcome = {
393
- id: step.id,
394
- status: "skipped",
395
- taskId: null,
396
- result: { summary: "skipped due to earlier failure" },
397
- };
398
- resultsById.set(step.id, outcome);
399
- preOutcomes.push(outcome);
400
- continue;
401
- }
402
- // when-gate
403
- let runThis = true;
404
- try {
405
- runThis = shouldRunStep(step, resultsById);
406
- } catch (err) {
407
- anyFailure = true;
408
- const outcome = {
409
- id: step.id,
410
- status: "failed",
411
- taskId: null,
412
- result: { summary: err.message },
413
- };
414
- resultsById.set(step.id, outcome);
415
- preOutcomes.push(outcome);
416
- continue;
417
- }
418
- if (!runThis) {
419
- const outcome = {
420
- id: step.id,
421
- status: "skipped",
422
- taskId: null,
423
- result: { summary: "when-condition false" },
424
- };
425
- resultsById.set(step.id, outcome);
426
- preOutcomes.push(outcome);
427
- continue;
428
- }
429
- // forEach-expansion
430
- if (step.forEach !== undefined) {
431
- let items;
914
+ for (const chunk of chunks) {
915
+ // Expand forEach / when into concrete tasks for this chunk
916
+ const runnable = []; // { step, message, recordId, parentId }
917
+ const preOutcomes = []; // outcomes produced synchronously (skipped)
918
+ for (const step of chunk) {
919
+ if (anyFailure && !continueOnError) {
920
+ const outcome = {
921
+ id: step.id,
922
+ status: "skipped",
923
+ taskId: null,
924
+ result: { summary: "skipped due to earlier failure" },
925
+ };
926
+ resultsById.set(step.id, outcome);
927
+ preOutcomes.push(outcome);
928
+ continue;
929
+ }
930
+ // when-gate
931
+ let runThis = true;
432
932
  try {
433
- items = resolveForEachItems(step.forEach, resultsById);
933
+ runThis = shouldRunStep(step, resultsById);
434
934
  } catch (err) {
435
935
  anyFailure = true;
436
936
  const outcome = {
@@ -443,97 +943,132 @@ export async function executeWorkflow(options = {}) {
443
943
  preOutcomes.push(outcome);
444
944
  continue;
445
945
  }
446
- if (items.length === 0) {
946
+ if (!runThis) {
447
947
  const outcome = {
448
948
  id: step.id,
449
949
  status: "skipped",
450
950
  taskId: null,
451
- result: { summary: "forEach items empty" },
951
+ result: { summary: "when-condition false" },
452
952
  };
453
953
  resultsById.set(step.id, outcome);
454
954
  preOutcomes.push(outcome);
455
955
  continue;
456
956
  }
457
- for (let k = 0; k < items.length; k++) {
458
- const childId = `${step.id}[${k}]`;
459
- const withItem = substituteItem(step.message, items[k]);
460
- const msg = substitutePlaceholders(withItem, resultsById);
957
+ // loop node runs its body repeatedly; per-iteration substitution
958
+ // happens inside runLoopStep, so push the raw template.
959
+ if (isLoopStep(step)) {
461
960
  runnable.push({
462
961
  step,
463
- message: msg,
464
- recordId: childId,
465
- parentId: step.id,
962
+ message: step.message,
963
+ recordId: step.id,
964
+ parentId: null,
965
+ isLoop: true,
466
966
  });
967
+ continue;
467
968
  }
468
- continue;
969
+ // forEach-expansion
970
+ if (step.forEach !== undefined) {
971
+ let items;
972
+ try {
973
+ items = resolveForEachItems(step.forEach, resultsById);
974
+ } catch (err) {
975
+ anyFailure = true;
976
+ const outcome = {
977
+ id: step.id,
978
+ status: "failed",
979
+ taskId: null,
980
+ result: { summary: err.message },
981
+ };
982
+ resultsById.set(step.id, outcome);
983
+ preOutcomes.push(outcome);
984
+ continue;
985
+ }
986
+ if (items.length === 0) {
987
+ const outcome = {
988
+ id: step.id,
989
+ status: "skipped",
990
+ taskId: null,
991
+ result: { summary: "forEach items empty" },
992
+ };
993
+ resultsById.set(step.id, outcome);
994
+ preOutcomes.push(outcome);
995
+ continue;
996
+ }
997
+ for (let k = 0; k < items.length; k++) {
998
+ const childId = `${step.id}[${k}]`;
999
+ const withItem = substituteItem(step.message, items[k]);
1000
+ const msg = substitutePlaceholders(withItem, resultsById);
1001
+ runnable.push({
1002
+ step,
1003
+ message: msg,
1004
+ recordId: childId,
1005
+ parentId: step.id,
1006
+ });
1007
+ }
1008
+ continue;
1009
+ }
1010
+ const message = substitutePlaceholders(step.message, resultsById);
1011
+ runnable.push({ step, message, recordId: step.id, parentId: null });
469
1012
  }
470
- const message = substitutePlaceholders(step.message, resultsById);
471
- runnable.push({ step, message, recordId: step.id, parentId: null });
472
- }
473
1013
 
474
- const promises = runnable.map(async ({ step, message, recordId }) => {
475
- if (onStepStart) onStepStart({ stepId: recordId, message });
476
- try {
477
- const entry = await _deps.runTask({
478
- templateId: step.templateId || null,
479
- userMessage: message,
480
- files: step.files || [],
481
- cwd,
482
- llmOptions,
483
- });
484
- const outcome = {
485
- id: recordId,
486
- status: entry.status,
487
- taskId: entry.taskId,
488
- result: entry.result,
489
- };
490
- resultsById.set(recordId, outcome);
491
- if (entry.status !== "completed") anyFailure = true;
492
- if (onStepComplete) onStepComplete(outcome);
493
- return outcome;
494
- } catch (err) {
495
- anyFailure = true;
496
- const outcome = {
497
- id: recordId,
498
- status: "failed",
1014
+ const promises = runnable.map(
1015
+ async ({ step, message, recordId, isLoop }) => {
1016
+ if (onStepStart) onStepStart({ stepId: recordId, message });
1017
+ let outcome;
1018
+ if (isLoop) {
1019
+ outcome = await runLoopStep({
1020
+ step,
1021
+ recordId,
1022
+ cwd,
1023
+ llmOptions,
1024
+ resultsById,
1025
+ });
1026
+ } else {
1027
+ const r = await runStepWithRetry({
1028
+ step,
1029
+ message,
1030
+ cwd,
1031
+ llmOptions,
1032
+ });
1033
+ outcome = outcomeFromRetry(recordId, r);
1034
+ }
1035
+ if (outcome.status !== "completed") anyFailure = true;
1036
+ resultsById.set(recordId, outcome);
1037
+ if (onStepComplete) onStepComplete(outcome);
1038
+ return outcome;
1039
+ },
1040
+ );
1041
+
1042
+ const results = await Promise.all(promises);
1043
+ stepOutcomes.push(...preOutcomes, ...results);
1044
+
1045
+ // Aggregate forEach children into a parent entry so downstream
1046
+ // `${step.<parent>.summary}` references still work.
1047
+ const byParent = new Map();
1048
+ for (let k = 0; k < runnable.length; k++) {
1049
+ const r = runnable[k];
1050
+ if (!r.parentId) continue;
1051
+ if (!byParent.has(r.parentId)) byParent.set(r.parentId, []);
1052
+ byParent.get(r.parentId).push(results[k]);
1053
+ }
1054
+ for (const [parentId, children] of byParent) {
1055
+ const allOk = children.every((c) => c.status === "completed");
1056
+ const anyOk = children.some((c) => c.status === "completed");
1057
+ const status = allOk ? "completed" : anyOk ? "partial" : "failed";
1058
+ resultsById.set(parentId, {
1059
+ id: parentId,
1060
+ status,
499
1061
  taskId: null,
500
- result: { summary: `Step threw: ${err.message}` },
501
- };
502
- resultsById.set(recordId, outcome);
503
- if (onStepComplete) onStepComplete(outcome);
504
- return outcome;
1062
+ result: {
1063
+ summary: children.map((c) => c.result?.summary ?? "").join("\n"),
1064
+ children: children.length,
1065
+ },
1066
+ });
505
1067
  }
506
- });
507
-
508
- const results = await Promise.all(promises);
509
- stepOutcomes.push(...preOutcomes, ...results);
510
-
511
- // Aggregate forEach children into a parent entry so downstream
512
- // `${step.<parent>.summary}` references still work.
513
- const byParent = new Map();
514
- for (let k = 0; k < runnable.length; k++) {
515
- const r = runnable[k];
516
- if (!r.parentId) continue;
517
- if (!byParent.has(r.parentId)) byParent.set(r.parentId, []);
518
- byParent.get(r.parentId).push(results[k]);
519
- }
520
- for (const [parentId, children] of byParent) {
521
- const allOk = children.every((c) => c.status === "completed");
522
- const anyOk = children.some((c) => c.status === "completed");
523
- const status = allOk ? "completed" : anyOk ? "partial" : "failed";
524
- resultsById.set(parentId, {
525
- id: parentId,
526
- status,
527
- taskId: null,
528
- result: {
529
- summary: children.map((c) => c.result?.summary ?? "").join("\n"),
530
- children: children.length,
531
- },
532
- });
533
1068
  }
534
- }
535
1069
 
536
- if (anyFailure && !continueOnError) break;
1070
+ if (anyFailure && !continueOnError) break;
1071
+ }
537
1072
  }
538
1073
 
539
1074
  const finishedAt = new Date(_deps.now()).toISOString();