@goodandready/dsh-moa 0.2.36 → 0.2.38

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.
@@ -99,6 +99,7 @@ export async function runReferencesParallel(references, messages, options = {},
99
99
  }
100
100
 
101
101
  const runOne = async () => {
102
+ let candidateTimer = null
102
103
  try {
103
104
  const callPromise = callWithTransientRetry(
104
105
  callLlm,
@@ -115,14 +116,12 @@ export async function runReferencesParallel(references, messages, options = {},
115
116
  )
116
117
 
117
118
  const timeoutPromise = new Promise((_, reject) => {
118
- const timer = setTimeout(() => {
119
+ candidateTimer = setTimeout(() => {
119
120
  bestEffort('candidate timeout abort', () => {
120
121
  abortControllers[i].abort(new Error(`Timeout after ${Math.round(timeoutMs / 1000)}s`))
121
122
  })
122
123
  reject(new Error(`Timeout after ${Math.round(timeoutMs / 1000)}s`))
123
124
  }, timeoutMs)
124
- timer.unref?.()
125
- abortControllers[i].signal.addEventListener('abort', () => clearTimeout(timer), { once: true })
126
125
  })
127
126
 
128
127
  const res = await Promise.race([callPromise, timeoutPromise])
@@ -153,16 +152,51 @@ export async function runReferencesParallel(references, messages, options = {},
153
152
  onProgress(`✅ *Candidate ${i + 1}/${total} (${label}, T=${effectiveTemperature}) finished${costStr}* | Live total: ~\$${currentTotalCost.toFixed(4)}\n`)
154
153
  }
155
154
  } catch (err) {
155
+ if (candidateTimer) clearTimeout(candidateTimer)
156
+
157
+ // If turn was aborted by caller, do NOT run fallbacks
158
+ if (options.signal?.aborted) {
159
+ finishedCount++
160
+ results[i] = {
161
+ index: i + 1,
162
+ slot,
163
+ label,
164
+ role_persona: role,
165
+ temperature: effectiveTemperature,
166
+ text: '[aborted]',
167
+ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
168
+ costUsd: 0,
169
+ ok: false,
170
+ error: 'Turn aborted',
171
+ }
172
+ return
173
+ }
174
+
156
175
  // Feature 10: Local fallback models if online candidate fails
157
176
  let fallbackRecovered = false
158
177
  if (options.local_fallback_enabled && Array.isArray(options.local_fallback_models) && options.local_fallback_models.length > 0) {
159
178
  for (const fbSlot of options.local_fallback_models) {
179
+ if (options.signal?.aborted) break
160
180
  const fbLabel = slotLabel(fbSlot)
181
+ const fbAbortCtrl = new AbortController()
182
+ let fbTimer = null
183
+ let removeParentAbort = null
184
+
185
+ if (options.signal) {
186
+ const onParentAbort = () => fbAbortCtrl.abort(options.signal.reason || new Error('Turn aborted'))
187
+ options.signal.addEventListener('abort', onParentAbort, { once: true })
188
+ removeParentAbort = () => options.signal.removeEventListener('abort', onParentAbort)
189
+ }
190
+
191
+ const fbTimeoutMs = typeof options.fallbackTimeoutMs === 'number' && options.fallbackTimeoutMs > 0
192
+ ? options.fallbackTimeoutMs
193
+ : Math.max(15000, timeoutMs)
194
+
161
195
  try {
162
196
  if (typeof onProgress === 'function') {
163
197
  onProgress(`🔄 *Candidate ${i + 1} (${label}) failed, attempting local fallback to ${fbLabel}...*\n`)
164
198
  }
165
- const fbRes = await callWithTransientRetry(
199
+ const fbCallPromise = callWithTransientRetry(
166
200
  callLlm,
167
201
  {
168
202
  provider: fbSlot.provider,
@@ -170,11 +204,22 @@ export async function runReferencesParallel(references, messages, options = {},
170
204
  messages: slotMessages,
171
205
  temperature: effectiveTemperature,
172
206
  maxTokens: options.maxTokens ?? 4096,
173
- timeoutMs,
174
- signal: abortControllers[i].signal,
207
+ timeoutMs: fbTimeoutMs,
208
+ signal: fbAbortCtrl.signal,
175
209
  },
176
210
  0
177
211
  )
212
+
213
+ const fbTimeoutPromise = new Promise((_, reject) => {
214
+ fbTimer = setTimeout(() => {
215
+ bestEffort('fallback timeout abort', () => {
216
+ fbAbortCtrl.abort(new Error(`Timeout after ${Math.round(fbTimeoutMs / 1000)}s`))
217
+ })
218
+ reject(new Error(`Timeout after ${Math.round(fbTimeoutMs / 1000)}s`))
219
+ }, fbTimeoutMs)
220
+ })
221
+
222
+ const fbRes = await Promise.race([fbCallPromise, fbTimeoutPromise])
178
223
  const fbText = typeof fbRes === 'string' ? fbRes : (fbRes?.content || fbRes?.text || '')
179
224
  const fbUsage = (typeof fbRes === 'object' && fbRes?.usage) ? fbRes.usage : {
180
225
  inputTokens: Math.max(1, Math.round(slotMessages.map((m) => m.content).join('').length / 4)),
@@ -202,6 +247,9 @@ export async function runReferencesParallel(references, messages, options = {},
202
247
  break
203
248
  } catch (fbErr) {
204
249
  logger.warn(`[dsh-moa] Fallback ${fbLabel} also failed:`, fbErr?.message || fbErr)
250
+ } finally {
251
+ if (fbTimer) clearTimeout(fbTimer)
252
+ if (removeParentAbort) removeParentAbort()
205
253
  }
206
254
  }
207
255
  }
package/lib/moa-llm.js CHANGED
@@ -1,17 +1,49 @@
1
+ function combineSignals(signal, timeoutSignal) {
2
+ if (!signal) return timeoutSignal
3
+ if (typeof AbortSignal.any === 'function') {
4
+ try {
5
+ return AbortSignal.any([signal, timeoutSignal])
6
+ } catch {
7
+ // fallback if signal is not an instance of AbortSignal
8
+ }
9
+ }
10
+ const combinedCtrl = new AbortController()
11
+ const onAbort = () => {
12
+ const reason = signal.aborted ? (signal.reason || new Error('Turn aborted')) : timeoutSignal.reason
13
+ combinedCtrl.abort(reason)
14
+ }
15
+ if (signal.aborted || timeoutSignal.aborted) {
16
+ onAbort()
17
+ return combinedCtrl.signal
18
+ }
19
+ signal.addEventListener?.('abort', onAbort, { once: true })
20
+ timeoutSignal.addEventListener?.('abort', onAbort, { once: true })
21
+ return combinedCtrl.signal
22
+ }
23
+
1
24
  /**
2
25
  * Unified LLM Dispatcher using ctx.llm.prepareCall / stream
3
26
  */
4
27
 
5
28
  export function createLlmCaller(ctx) {
6
- return async ({ provider, model, messages, temperature = 0.6, maxTokens = 4096, timeoutMs, onStreamDelta }) => {
29
+ return async ({ provider, model, messages, temperature = 0.6, maxTokens = 4096, timeoutMs, onStreamDelta, signal, stop, reasoningEffort }) => {
7
30
  if (!ctx.llm) {
8
31
  throw new Error('ctx.llm is not available in cordis context')
9
32
  }
10
33
 
34
+ const callConfig = {
35
+ provider,
36
+ model,
37
+ ...(temperature !== undefined ? { temperature } : {}),
38
+ ...(maxTokens !== undefined ? { maxTokens } : {}),
39
+ ...(stop !== undefined ? { stop } : {}),
40
+ ...(reasoningEffort !== undefined ? { reasoningEffort } : {}),
41
+ }
42
+
11
43
  let prep
12
44
  try {
13
- // DSH 0.1.2-rc.1 API expects { provider, model } object
14
- prep = await ctx.llm.prepareCall({ provider, model })
45
+ // DSH 0.2.0-rc.2 API expects LlmCallConfig object, supports signal
46
+ prep = await ctx.llm.prepareCall(callConfig, signal)
15
47
  } catch (err) {
16
48
  if (typeof ctx.llm.prepareCall === 'function') {
17
49
  try {
@@ -27,23 +59,33 @@ export function createLlmCaller(ctx) {
27
59
  throw new Error(`LLM provider/model ${provider}:${model} could not be prepared`)
28
60
  }
29
61
 
62
+ if (signal?.aborted) {
63
+ throw (signal.reason || new Error('Turn aborted'))
64
+ }
65
+
30
66
  const effectiveTimeoutMs = typeof timeoutMs === 'number' && timeoutMs > 0 ? timeoutMs : 120000
31
67
  const abortCtrl = new AbortController()
32
68
  const timeoutId = setTimeout(() => abortCtrl.abort(new Error(`LLM call timeout after ${Math.round(effectiveTimeoutMs / 1000)}s`)), effectiveTimeoutMs)
33
69
  timeoutId.unref?.()
70
+ const combinedSignal = combineSignals(signal, abortCtrl.signal)
34
71
 
35
72
  try {
73
+ const streamConfig = prep.config || callConfig
36
74
  const stream = prep.stream({
75
+ ...streamConfig,
37
76
  messages,
38
- temperature,
39
- maxTokens,
40
- signal: abortCtrl.signal,
77
+ temperature: streamConfig.temperature ?? temperature,
78
+ maxTokens: streamConfig.maxTokens ?? maxTokens,
79
+ signal: combinedSignal,
41
80
  })
42
81
 
43
82
  let fullText = ''
44
83
  let usageInfo = null
45
84
 
46
85
  for await (const chunk of stream) {
86
+ if (combinedSignal?.aborted) {
87
+ throw (combinedSignal.reason || new Error('Turn aborted'))
88
+ }
47
89
  if (chunk.type === 'text-delta' && typeof chunk.text === 'string') {
48
90
  fullText += chunk.text
49
91
  if (typeof onStreamDelta === 'function') {
@@ -53,9 +95,35 @@ export function createLlmCaller(ctx) {
53
95
  if (!fullText) fullText = chunk.block.text
54
96
  } else if (chunk.type === 'usage' && chunk.usage) {
55
97
  usageInfo = chunk.usage
98
+ } else if (chunk.type === 'finish') {
99
+ const reason = chunk.reason
100
+ if (reason && (reason.kind === 'error' || reason.kind === 'aborted')) {
101
+ const failure = reason.failure || {}
102
+ const errMsg = failure.message || failure.error?.message || reason.message || reason.error?.message || `LLM stream finished with ${reason.kind}`
103
+ const err = new Error(errMsg)
104
+ err.code = failure.code || reason.code || (reason.kind === 'error' ? 'LLM_ERROR' : 'ABORT_ERR')
105
+ if (failure.cause) err.cause = failure.cause
106
+ throw err
107
+ }
108
+ } else if (chunk.type === 'error') {
109
+ const errMsg = chunk.error?.message || chunk.message || 'LLM stream error'
110
+ const err = new Error(errMsg)
111
+ err.code = chunk.code || chunk.error?.code || 'LLM_ERROR'
112
+ if (chunk.error?.cause) err.cause = chunk.error.cause
113
+ throw err
56
114
  }
57
115
  }
58
116
 
117
+ if (combinedSignal?.aborted) {
118
+ throw (combinedSignal.reason || new Error('Turn aborted'))
119
+ }
120
+
121
+ if (!fullText.trim()) {
122
+ const err = new Error('LLM call returned empty response')
123
+ err.code = 'EMPTY_RESPONSE'
124
+ throw err
125
+ }
126
+
59
127
  clearTimeout(timeoutId)
60
128
  return {
61
129
  content: fullText,
package/lib/moa-runner.js CHANGED
@@ -12,7 +12,6 @@ import { executeMultiJudgePanel, buildCompositeBlockDirectives } from './moa-mul
12
12
  import { applyBudgetGuardrails } from './moa-budget.js'
13
13
  import { extractPriorTurnBaseline, pruneMultiTurnMessages } from './moa-context.js'
14
14
  import { generateMoABenchmarkReport } from './moa-report.js'
15
-
16
15
  // Re-exports for consumers & backward compatibility
17
16
  export { slotLabel, cleanAdvisoryMessages, isBroadPromptRequiringQuestions, buildQuestionSynthesisPrompt, buildCuratorSynthesisPrompt, buildSynthesisPrompt, ANTIPATTERNS_RUBRIC } from './moa-prompts.js'
18
17
  export { parseWinnerIndex, parseRecommendedAssembler, parseMoACommand, stripOrSummarizeCode, formatMoAResponse } from './moa-parser.js'
@@ -20,7 +19,6 @@ export { estimateTokenCost, summarizeMoAUsage } from './pricing.js'
20
19
  export { REFERENCE_SYSTEM_PROMPT, callWithTransientRetry, runReferencesParallel } from './moa-candidates.js'
21
20
  export { executeTestGateForCandidates, runCandidateTestGate } from './moa-test-gate.js'
22
21
  export { streamMoATurn } from './moa-stream.js'
23
-
24
22
  /**
25
23
  * Executes the full Mixture of Agents pipeline.
26
24
  */
@@ -45,6 +43,7 @@ export async function runMoAPipeline({
45
43
  force = isTestEnvironment() ? true : false,
46
44
  checkpoint = true,
47
45
  checkpointFn = null,
46
+ runId: customRunId = null,
48
47
  }) {
49
48
  const promoteOpts = {
50
49
  webServerPort,
@@ -56,16 +55,13 @@ export async function runMoAPipeline({
56
55
  }
57
56
  if (signal?.aborted) return { error: new Error('Turn aborted') }
58
57
  const startTime = Date.now()
59
-
60
58
  // 1. Resolve configurations
61
59
  const referenceModels = Array.isArray(preset?.reference_models) && preset.reference_models.length > 0
62
60
  ? preset.reference_models : [{ provider: '', model: '' }]
63
61
  const primaryJudge = preset?.aggregator?.provider && preset?.aggregator?.model
64
62
  ? preset.aggregator : { provider: '', model: '' }
65
-
66
63
  const fallbackJudges = Array.isArray(preset?.aggregator_fallbacks) ? preset.aggregator_fallbacks : []
67
64
  const judgesChain = [primaryJudge, ...fallbackJudges]
68
-
69
65
  const refTemp = typeof preset?.reference_temperature === 'number' ? preset.reference_temperature : 0.6
70
66
  const aggTemp = typeof preset?.aggregator_temperature === 'number' ? preset.aggregator_temperature : 0.4
71
67
  const maxTokens = typeof preset?.max_tokens === 'number' ? preset.max_tokens : 4096
@@ -76,8 +72,7 @@ export async function runMoAPipeline({
76
72
  const candidateRetries = 1, refTimeoutSec = typeof preset?.reference_timeout_sec === 'number' ? preset.reference_timeout_sec : 60
77
73
  const aggTimeoutSec = typeof preset?.aggregator_timeout_sec === 'number' ? preset.aggregator_timeout_sec : 180
78
74
  const isBlindEvaluation = Boolean(preset?.blind_evaluation), isPeerCritiqueEnabled = Boolean(preset?.peer_critique_enabled)
79
- const allowCandidateOverride = Boolean(preset?.allow_candidate_override), runId = crypto.randomUUID()
80
-
75
+ const allowCandidateOverride = Boolean(preset?.allow_candidate_override), runId = customRunId || null
81
76
  // Feature 6: Budget Guardrail check
82
77
  const budgetCheck = applyBudgetGuardrails({
83
78
  references: referenceModels,
@@ -109,7 +104,6 @@ export async function runMoAPipeline({
109
104
  if (budgetCheck.action === 'trim' && typeof onProgress === 'function') {
110
105
  onProgress(`✂️ *${budgetCheck.reason}*\n`)
111
106
  }
112
-
113
107
  // 2. Collect project context for refinement tasks
114
108
  const collectedCtx = await collectProjectContext(cwd, 16000)
115
109
  const isRefinement = Boolean(collectedCtx?.files?.length > 0 && isRefinementTask(userPrompt, collectedCtx.files))
@@ -128,19 +122,15 @@ export async function runMoAPipeline({
128
122
  skippedList: collectedCtx.skippedList,
129
123
  })
130
124
  }
131
-
132
125
  // 3. Build prompts & Feature 8 multi-turn context
133
126
  const priorTurnBaseline = preset?.multi_turn_enabled !== false ? extractPriorTurnBaseline(messages) : null
134
127
  const prunedHistory = preset?.multi_turn_enabled !== false ? pruneMultiTurnMessages(messages) : messages
135
-
136
128
  const candidateSystemPrompt = projectContext
137
129
  ? `${REFERENCE_SYSTEM_PROMPT}\n\n### Current Project Files & Context:\n${projectContext}`
138
130
  : REFERENCE_SYSTEM_PROMPT
139
-
140
131
  const askClarifyingQuestions = preset?.ask_clarifying_questions !== false
141
132
  const needsQuestions = askClarifyingQuestions && isBroadPromptRequiringQuestions(userPrompt, messages)
142
133
  const enrichedMessages = [...prunedHistory, { role: 'user', content: userPrompt }]
143
-
144
134
  // 4. Parallel fan-out to candidate models with temperature gradient & local fallback
145
135
  const referenceOutputs = await runReferencesParallel(
146
136
  effectiveReferences,
@@ -162,12 +152,10 @@ export async function runMoAPipeline({
162
152
  callLlm,
163
153
  onProgress
164
154
  )
165
-
166
155
  if (signal?.aborted) {
167
- await cleanMoaWorkspaces(cwd)
156
+ await cleanMoaWorkspaces(cwd, { runId })
168
157
  return { error: new Error('Turn aborted') }
169
158
  }
170
-
171
159
  // Fail fast if all candidates failed
172
160
  const successfulRefs = referenceOutputs.filter((r) => r.ok)
173
161
  if (successfulRefs.length === 0) {
@@ -194,7 +182,6 @@ export async function runMoAPipeline({
194
182
  durationMs: Date.now() - startTime,
195
183
  }
196
184
  }
197
-
198
185
  // 5. Extract file blocks & write candidate workspaces
199
186
  for (let i = 0; i < referenceOutputs.length; i++) {
200
187
  const ref = referenceOutputs[i]
@@ -203,19 +190,21 @@ export async function runMoAPipeline({
203
190
  ref.files = files
204
191
  if (files.length > 0) {
205
192
  ref.syntaxWarning = (verifyFileSyntax(files) || []).map((w) => `${w.file}: ${w.error}`).join('; ')
206
- await writeCandidateWorkspace(cwd, i + 1, files)
193
+ await writeCandidateWorkspace(cwd, i + 1, files, { runId })
207
194
  }
208
195
  }
209
-
196
+ if (signal?.aborted) {
197
+ await cleanMoaWorkspaces(cwd, { runId })
198
+ return { error: new Error('Turn aborted') }
199
+ }
210
200
  // 5b. Test Execution Gate
211
201
  await executeTestGateForCandidates({
212
202
  cwd,
213
203
  referenceOutputs,
214
204
  preset,
215
- options: { testGateEnabled, testCommand, execFn },
205
+ options: { testGateEnabled, testCommand, execFn, runId },
216
206
  onProgress,
217
207
  })
218
-
219
208
  // 6. Questionnaire synthesis branch
220
209
  if (needsQuestions) {
221
210
  if (typeof onProgress === 'function') {
@@ -243,12 +232,10 @@ export async function runMoAPipeline({
243
232
  logger.warn('[dsh-moa] Questionnaire synthesis failed, proceeding with fallback questions:', err)
244
233
  questionsContent = `### Clarification of Requirements: "${userPrompt}"\n\n1. **Architecture & Scope**: Single-file deliverable or multi-module project structure?\n2. **Design & Style**: Minimalist, dark mode, or clean neutral theme?\n3. **Functional Priorities**: Core MVP or comprehensive extended implementation?\n\n*Reply with your preferences (e.g. "1, 2") or proceed with defaults.*`
245
234
  }
246
-
247
- await cleanMoaWorkspaces(cwd)
235
+ await cleanMoaWorkspaces(cwd, { runId })
248
236
  const totalTokens = referenceOutputs.reduce((acc, r) => acc + (r.usage?.totalTokens || 0), 0) + qUsage.totalTokens
249
237
  const totalCostUsd = Number((referenceOutputs.reduce((acc, r) => acc + (r.costUsd || 0), 0) + qUsage.costUsd).toFixed(5))
250
238
  const durationMs = Date.now() - startTime
251
-
252
239
  try {
253
240
  await recordMoaRunAsync({
254
241
  prompt: userPrompt,
@@ -266,7 +253,6 @@ export async function runMoAPipeline({
266
253
  } catch (histErr) {
267
254
  logger.warn('[dsh-moa] Failed to record questionnaire run in history:', histErr?.message || histErr)
268
255
  }
269
-
270
256
  return {
271
257
  kind: 'questions',
272
258
  content: questionsContent,
@@ -292,12 +278,16 @@ export async function runMoAPipeline({
292
278
 
293
279
  // 7. Fast Mode (1 candidate, bypass judge)
294
280
  if (isFastMode) {
281
+ if (signal?.aborted) {
282
+ await cleanMoaWorkspaces(cwd, { runId })
283
+ return { error: new Error('Turn aborted') }
284
+ }
295
285
  const single = successfulRefs[0]
296
286
  let promotedFiles = []
297
287
  if (single.files?.length > 0) {
298
- promotedFiles = await promoteCandidateWorkspace(cwd, single.index, promoteOpts)
288
+ promotedFiles = await promoteCandidateWorkspace(cwd, single.index, { ...promoteOpts, runId })
299
289
  } else {
300
- await cleanMoaWorkspaces(cwd)
290
+ await cleanMoaWorkspaces(cwd, { runId })
301
291
  }
302
292
 
303
293
  const livePreview = await createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs)
@@ -348,6 +338,11 @@ export async function runMoAPipeline({
348
338
  }
349
339
  }
350
340
 
341
+ if (signal?.aborted) {
342
+ await cleanMoaWorkspaces(cwd, { runId })
343
+ return { error: new Error('Turn aborted') }
344
+ }
345
+
351
346
  // 7b. Consilium Round 2 (Peer Critique) if enabled
352
347
  if (isPeerCritiqueEnabled && successfulRefs.length > 1) {
353
348
  if (typeof onProgress === 'function') {
@@ -379,7 +374,7 @@ export async function runMoAPipeline({
379
374
  if (r2Files.length > 0) {
380
375
  cand.files = r2Files
381
376
  cand.syntaxWarning = (verifyFileSyntax(r2Files) || []).map((w) => `${w.file}: ${w.error}`).join('; ')
382
- await writeCandidateWorkspace(cwd, cand.index, r2Files)
377
+ await writeCandidateWorkspace(cwd, cand.index, r2Files, { runId })
383
378
  }
384
379
  if (typeof res === 'object' && res?.usage) {
385
380
  cand.usage.inputTokens = (cand.usage.inputTokens || 0) + (res.usage.inputTokens || 0)
@@ -397,13 +392,18 @@ export async function runMoAPipeline({
397
392
  cwd,
398
393
  referenceOutputs: successfulRefs,
399
394
  preset,
400
- options: { testGateEnabled, testCommand, execFn },
395
+ options: { testGateEnabled, testCommand, execFn, runId },
401
396
  onProgress,
402
397
  })
403
398
  }
404
399
 
405
400
  // 7c. Feature 2: Multi-Judge Panel & Consensus Voting
406
401
  let multiJudgeResult = null
402
+ if (signal?.aborted) {
403
+ await cleanMoaWorkspaces(cwd, { runId })
404
+ return { error: new Error('Turn aborted') }
405
+ }
406
+
407
407
  if (preset?.multi_judge_enabled && successfulRefs.length > 1) {
408
408
  multiJudgeResult = await executeMultiJudgePanel({
409
409
  judges: (preset.judge_models && preset.judge_models.length > 0) ? preset.judge_models : [primaryJudge],
@@ -488,6 +488,11 @@ export async function runMoAPipeline({
488
488
  successfulRefs.map((r, i) => `### Candidate ${i + 1} (${r.label})\n${r.text}`).join('\n\n')
489
489
  }
490
490
 
491
+ if (signal?.aborted) {
492
+ await cleanMoaWorkspaces(cwd, { runId })
493
+ return { error: new Error('Turn aborted') }
494
+ }
495
+
491
496
  // 9. Evaluate winner & promote files
492
497
  const winningIndex = (multiJudgeResult?.consensus && multiJudgeResult?.winningCandidateIndex)
493
498
  ? multiJudgeResult.winningCandidateIndex
@@ -503,15 +508,15 @@ export async function runMoAPipeline({
503
508
 
504
509
  const keepWorkspaces = allowCandidateOverride
505
510
  if (synthesizedFiles.length > 0) {
506
- await writeCandidateWorkspace(cwd, 'curator-synthesis', synthesizedFiles)
507
- promotedFiles = await promoteCandidateWorkspace(cwd, 'curator-synthesis', { ...promoteOpts, keepMoa: keepWorkspaces })
511
+ await writeCandidateWorkspace(cwd, 'curator-synthesis', synthesizedFiles, { runId })
512
+ promotedFiles = await promoteCandidateWorkspace(cwd, 'curator-synthesis', { ...promoteOpts, runId, keepMoa: keepWorkspaces })
508
513
  } else if (referenceOutputs[winningIndex - 1]?.files?.length > 0) {
509
- promotedFiles = await promoteCandidateWorkspace(cwd, winningIndex, { ...promoteOpts, keepMoa: keepWorkspaces })
514
+ promotedFiles = await promoteCandidateWorkspace(cwd, winningIndex, { ...promoteOpts, runId, keepMoa: keepWorkspaces })
510
515
  } else if (successfulRefs[0]?.files?.length > 0) {
511
516
  const fallbackIdx = successfulRefs[0].index
512
- promotedFiles = await promoteCandidateWorkspace(cwd, fallbackIdx, { ...promoteOpts, keepMoa: keepWorkspaces })
517
+ promotedFiles = await promoteCandidateWorkspace(cwd, fallbackIdx, { ...promoteOpts, runId, keepMoa: keepWorkspaces })
513
518
  } else if (!keepWorkspaces) {
514
- await cleanMoaWorkspaces(cwd)
519
+ await cleanMoaWorkspaces(cwd, { runId })
515
520
  }
516
521
 
517
522
  const livePreview = await createPromotedPreview(liveCanvas, cwd, promotedFiles, referenceOutputs)
package/lib/moa-stream.js CHANGED
@@ -1,3 +1,4 @@
1
+ import crypto from 'node:crypto'
1
2
  /**
2
3
  * Zero-latency Live Streaming Generator for MoA Turns
3
4
  */
@@ -11,6 +12,7 @@ import { runMoAPipeline } from './moa-runner.js'
11
12
  */
12
13
  export async function* streamMoATurn({ targetPreset, userPrompt, messages, callLlm, cwd, prices, historyFilePath, liveCanvas, webServerPort, timeMachineEngine, force }, options = {}) {
13
14
  const signal = options?.signal
15
+ const runId = options?.runId || null
14
16
  if (signal?.aborted) return
15
17
 
16
18
  const internalAbortCtrl = new AbortController()
@@ -69,7 +71,7 @@ export async function* streamMoATurn({ targetPreset, userPrompt, messages, callL
69
71
 
70
72
  while (!done || queue.length > 0) {
71
73
  if (combinedSignal.aborted) {
72
- await cleanMoaWorkspaces(cwd)
74
+ await cleanMoaWorkspaces(cwd, runId ? { runId } : {})
73
75
  return
74
76
  }
75
77
 
@@ -96,7 +98,7 @@ export async function* streamMoATurn({ targetPreset, userPrompt, messages, callL
96
98
 
97
99
  const result = await pipelinePromise
98
100
  if (combinedSignal.aborted) {
99
- await cleanMoaWorkspaces(cwd)
101
+ await cleanMoaWorkspaces(cwd, runId ? { runId } : {})
100
102
  return
101
103
  }
102
104
 
@@ -126,7 +128,7 @@ export async function* streamMoATurn({ targetPreset, userPrompt, messages, callL
126
128
  } finally {
127
129
  internalAbortCtrl.abort()
128
130
  if (!done) {
129
- await cleanMoaWorkspaces(cwd).catch(() => {})
131
+ await cleanMoaWorkspaces(cwd, runId ? { runId } : {}).catch(() => {})
130
132
  }
131
133
  }
132
134
  }