@goodandready/dsh-moa 0.2.15 → 0.2.17

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.
package/lib/index.js CHANGED
@@ -19,17 +19,13 @@ import {
19
19
  streamMoATurn,
20
20
  } from './moa-runner.js'
21
21
 
22
- import {
23
- getMoaHistory,
24
- getMoaLeaderboard,
25
- getMoaRunById,
26
- exportMoaHistory,
27
- } from './history.js'
28
- import { promoteCandidateWorkspace } from './file-workspace.js'
22
+ import { registerMoaRoutes } from './routes.js'
29
23
 
30
24
  import { createLiveCanvasClient } from './live-canvas.js'
25
+ import { registerPluginUpdater, isSafeWriteRequest } from './updater.js'
31
26
 
32
- export const name = 'dsh-moa'
27
+ export const name = '@goodandready/dsh-moa'
28
+ export { collectProjectContext, formatProjectContext } from './file-workspace.js'
33
29
  export const inject = ['webServer', 'llm', 'settings', 'sessions', 'tools']
34
30
 
35
31
  export const NS = 'dsh-moa'
@@ -117,28 +113,6 @@ export const Config = z.object({
117
113
  ]),
118
114
  })
119
115
 
120
- function writeJson(res, status, body) {
121
- res.writeHead(status, {
122
- 'Content-Type': 'application/json; charset=utf-8',
123
- 'Cache-Control': 'no-store',
124
- })
125
- res.end(JSON.stringify(body))
126
- }
127
-
128
- function readBody(req) {
129
- return new Promise((resolve, reject) => {
130
- let acc = ''
131
- req.on('data', (chunk) => {
132
- acc += chunk
133
- if (acc.length > 5 * 1024 * 1024) {
134
- req.destroy()
135
- reject(new Error('Payload Too Large'))
136
- }
137
- })
138
- req.on('end', () => resolve(acc))
139
- req.on('error', reject)
140
- })
141
- }
142
116
 
143
117
  export function apply(ctx, config) {
144
118
  refreshCatalogInBackground()
@@ -238,361 +212,23 @@ export function apply(ctx, config) {
238
212
  const liveCanvas = createLiveCanvasClient({ getPort: () => ctx.webServer?.port })
239
213
 
240
214
  // Route: /dsh-moa/status
215
+ // Register one-click updater endpoint
241
216
  ctx.effect(() => {
242
- return ctx.webServer.register({
243
- kind: 'exact',
244
- path: '/dsh-moa/status',
245
- handler: (_req, res) => {
246
- const cfg = live()
247
- writeJson(res, 200, {
248
- ok: true,
249
- enabled: cfg.enabled !== false,
250
- defaultPreset: cfg.default_preset || 'default',
251
- presetsCount: (cfg.presets || []).length,
252
- })
253
- },
217
+ return registerPluginUpdater(ctx, {
218
+ endpoint: '/api/dsh-moa/update',
219
+ packageName: '@goodandready/dsh-moa',
220
+ manifestUrl: new URL('../package.json', import.meta.url),
254
221
  })
255
- }, 'dsh-moa: status route')
256
-
257
- // Route: /dsh-moa/models
258
- ctx.effect(() => {
259
- return ctx.webServer.register({
260
- kind: 'exact',
261
- path: '/dsh-moa/models',
262
- handler: async (_req, res) => {
263
- const result = []
264
- const seen = new Set()
265
-
266
- const add = (p, m) => {
267
- if (!p || !m) return
268
- const key = `${p}:${m}`
269
- if (!seen.has(key)) {
270
- seen.add(key)
271
- result.push({ provider: p, model: m, label: key })
272
- }
273
- }
274
-
275
- if (ctx.llm) {
276
- try {
277
- if (typeof ctx.llm.listAvailableModels === 'function') {
278
- const list = await ctx.llm.listAvailableModels()
279
- for (const item of (list || [])) {
280
- if (item?.provider && item?.id) add(item.provider, item.id)
281
- }
282
- }
283
- } catch (e) {
284
- console.warn('[dsh-moa] ctx.llm.listAvailableModels error:', e)
285
- }
286
-
287
- try {
288
- if (typeof ctx.llm.listConfigurableProviders === 'function') {
289
- const cProvs = ctx.llm.listConfigurableProviders() || []
290
- for (const cp of cProvs) {
291
- const cpId = cp.provider || cp.id
292
- if (cpId && typeof ctx.llm.listModels === 'function') {
293
- try {
294
- const mList = await ctx.llm.listModels(cpId)
295
- for (const m of (mList || [])) {
296
- const mId = typeof m === 'string' ? m : (m.id || m.model || m.name)
297
- if (mId) add(cpId, mId)
298
- }
299
- } catch {}
300
- }
301
- }
302
- }
303
- } catch (e) {
304
- console.warn('[dsh-moa] ctx.llm.listConfigurableProviders error:', e)
305
- }
306
- }
307
-
308
- // Query settings for configured providers
309
- try {
310
- const s = ctx.get ? ctx.get('settings') : ctx.settings
311
- const getSetting = (k) => {
312
- try { return s?.get?.(k) } catch { return undefined }
313
- }
314
-
315
- const def = getSetting('agent-default-model')
316
- if (def?.provider && def?.model) {
317
- add(def.provider, def.model)
318
- }
319
-
320
- const piAi = getSetting('llm-pi-ai')
321
- for (const [pName, pData] of Object.entries(piAi?.providers || {})) {
322
- for (const m of (pData?.models || [])) {
323
- const mId = typeof m === 'string' ? m : (m?.id || m?.model)
324
- if (mId) add(pName, mId)
325
- }
326
- }
327
-
328
- const sync = getSetting('dsh-model-sync')
329
- for (const [syncProv, syncModels] of Object.entries(sync?.modelCatalogs || {})) {
330
- for (const m of (syncModels || [])) {
331
- const mId = typeof m === 'string' ? m : (m?.id || m?.model)
332
- if (mId) add(syncProv, mId)
333
- }
334
- }
335
- } catch (e) {
336
- console.warn('[dsh-moa] Error reading settings:', e)
337
- }
338
-
339
- writeJson(res, 200, { ok: true, models: result })
340
- },
341
- })
342
- }, 'dsh-moa: models route')
343
-
344
- // Route: /dsh-moa/presets
345
- ctx.effect(() => {
346
- return ctx.webServer.register({
347
- kind: 'exact',
348
- path: '/dsh-moa/presets',
349
- handler: async (req, res) => {
350
- const cfg = live()
351
- if (req.method === 'GET') {
352
- writeJson(res, 200, {
353
- ok: true,
354
- defaultPreset: cfg.default_preset || 'default',
355
- presets: cfg.presets || [],
356
- })
357
- return
358
- }
359
-
360
- if (req.method === 'POST' || req.method === 'PUT') {
361
- try {
362
- const raw = await readBody(req)
363
- const payload = JSON.parse(raw)
364
- const newConfig = {
365
- enabled: typeof payload.enabled === 'boolean' ? payload.enabled : cfg.enabled !== false,
366
- default_preset: payload.default_preset || cfg.default_preset || 'default',
367
- presets: Array.isArray(payload.presets) ? payload.presets : (cfg.presets || []),
368
- }
369
-
370
- // Validate the merged payload through the plugin schema before it
371
- // reaches the settings storage: malformed payloads must be
372
- // rejected with 400 instead of being persisted.
373
- let validated
374
- try {
375
- validated = Config(structuredClone(newConfig))
376
- } catch (verr) {
377
- writeJson(res, 400, { ok: false, error: 'Invalid presets payload: ' + (verr?.message || String(verr)) })
378
- return
379
- }
380
-
381
- // Persist via Cordis Settings API
382
- if (settingsScope && typeof settingsScope.replace === 'function') {
383
- await settingsScope.replace(validated)
384
- } else {
385
- // In-memory fallback if settings service is unavailable
386
- getConfig = () => validated
387
- }
388
-
389
- writeJson(res, 200, {
390
- ok: true,
391
- enabled: validated.enabled !== false,
392
- defaultPreset: validated.default_preset,
393
- presets: validated.presets,
394
- })
395
- } catch (err) {
396
- writeJson(res, 400, { ok: false, error: err?.message || String(err) })
397
- }
398
- return
399
- }
400
-
401
- writeJson(res, 405, { ok: false, error: 'Method Not Allowed' })
402
- },
403
- })
404
- }, 'dsh-moa: presets route')
405
-
406
- // Route: /dsh-moa/history (#9)
407
- ctx.effect(() => {
408
- return ctx.webServer.register({
409
- kind: 'exact',
410
- path: '/dsh-moa/history',
411
- handler: async (req, res) => {
412
- if (req.method !== 'GET') {
413
- writeJson(res, 405, { ok: false, error: 'GET required' })
414
- return
415
- }
416
- try {
417
- const u = new URL(req.url, 'http://localhost')
418
- const limit = parseInt(u.searchParams.get('limit') || '20', 10)
419
- const offset = parseInt(u.searchParams.get('offset') || '0', 10)
420
- const data = getMoaHistory(limit, offset)
421
- writeJson(res, 200, { ok: true, ...data })
422
- } catch (err) {
423
- writeJson(res, 500, { ok: false, error: err?.message || String(err) })
424
- }
425
- },
426
- })
427
- }, 'dsh-moa: history route')
428
-
429
- // Route: /dsh-moa/leaderboard
430
- ctx.effect(() => {
431
- return ctx.webServer.register({
432
- kind: 'exact',
433
- path: '/dsh-moa/leaderboard',
434
- handler: async (req, res) => {
435
- if (req.method !== 'GET') {
436
- writeJson(res, 405, { ok: false, error: 'GET required' })
437
- return
438
- }
439
- try {
440
- const url = new URL(req.url || '', 'http://127.0.0.1')
441
- const preset = url.searchParams.get('preset') || null
442
- const format = url.searchParams.get('format') || 'json'
443
-
444
- if (format === 'csv') {
445
- const csv = exportMoaHistory(undefined, { format: 'csv', preset })
446
- res.writeHead(200, {
447
- 'Content-Type': 'text/csv; charset=utf-8',
448
- 'Content-Disposition': 'attachment; filename="moa-leaderboard.csv"',
449
- })
450
- res.end(csv)
451
- return
452
- }
453
-
454
- const data = getMoaLeaderboard(undefined, { preset })
455
- writeJson(res, 200, { ok: true, ...data })
456
- } catch (err) {
457
- writeJson(res, 500, { ok: false, error: err?.message || String(err) })
458
- }
459
- },
460
- })
461
- }, 'dsh-moa: leaderboard route')
462
-
463
- // Route: /dsh-moa/promote (Candidate Override Action)
464
- ctx.effect(() => {
465
- return ctx.webServer.register({
466
- kind: 'exact',
467
- path: '/dsh-moa/promote',
468
- handler: async (req, res) => {
469
- if (req.method !== 'POST') {
470
- writeJson(res, 405, { ok: false, error: 'POST required' })
471
- return
472
- }
473
- try {
474
- const raw = await readBody(req)
475
- const body = JSON.parse(raw || '{}')
476
- const candidateIndex = parseInt(body?.candidateIndex, 10)
477
- if (isNaN(candidateIndex) || candidateIndex < 1) {
478
- writeJson(res, 400, { ok: false, error: 'Valid candidateIndex required (>= 1)' })
479
- return
480
- }
481
- const cwd = body?.cwd || process.cwd()
482
- const promotedFiles = await promoteCandidateWorkspace(cwd, candidateIndex, { keepMoa: true })
483
- writeJson(res, 200, { ok: true, candidateIndex, promotedFiles })
484
- } catch (err) {
485
- writeJson(res, 500, { ok: false, error: err?.message || String(err) })
486
- }
487
- },
488
- })
489
- }, 'dsh-moa: promote candidate route')
490
-
491
- // Route: /dsh-moa/history/export
492
- ctx.effect(() => {
493
- return ctx.webServer.register({
494
- kind: 'exact',
495
- path: '/dsh-moa/history/export',
496
- handler: async (req, res) => {
497
- if (req.method !== 'GET') {
498
- writeJson(res, 405, { ok: false, error: 'GET required' })
499
- return
500
- }
501
- try {
502
- const url = new URL(req.url || '', 'http://127.0.0.1')
503
- const preset = url.searchParams.get('preset') || null
504
- const format = url.searchParams.get('format') || 'json'
505
- const data = exportMoaHistory(undefined, { format, preset })
506
-
507
- if (format === 'csv') {
508
- res.writeHead(200, {
509
- 'Content-Type': 'text/csv; charset=utf-8',
510
- 'Content-Disposition': 'attachment; filename="moa-history.csv"',
511
- })
512
- res.end(data)
513
- return
514
- }
515
-
516
- res.writeHead(200, {
517
- 'Content-Type': 'application/json; charset=utf-8',
518
- 'Content-Disposition': 'attachment; filename="moa-history.json"',
519
- })
520
- res.end(data)
521
- } catch (err) {
522
- writeJson(res, 500, { ok: false, error: err?.message || String(err) })
523
- }
524
- },
525
- })
526
- }, 'dsh-moa: history export route')
527
-
528
- // Route: /dsh-moa/runs/<id> (run replay by id)
529
- ctx.effect(() => {
530
- return ctx.webServer.register({
531
- kind: 'prefix',
532
- path: '/dsh-moa/runs/',
533
- handler: (req, res) => {
534
- if (req.method !== 'GET') {
535
- writeJson(res, 405, { ok: false, error: 'GET required' })
536
- return
537
- }
538
- const urlPath = req.url.split('?')[0]
539
- const parts = urlPath.split('/').filter(Boolean)
540
- const runId = parts[2] || ''
541
- const run = runId ? getMoaRunById(runId) : null
542
- if (!run) {
543
- writeJson(res, 404, { ok: false, error: 'Run not found' })
544
- return
545
- }
546
- writeJson(res, 200, { ok: true, run })
547
- },
548
- })
549
- }, 'dsh-moa: run by id route')
550
-
551
- // Route: /dsh-moa/run
552
- ctx.effect(() => {
553
- return ctx.webServer.register({
554
- kind: 'exact',
555
- path: '/dsh-moa/run',
556
- handler: async (req, res) => {
557
- if (req.method !== 'POST') {
558
- writeJson(res, 405, { ok: false, error: 'POST required' })
559
- return
560
- }
561
-
562
- try {
563
- const raw = await readBody(req)
564
- const payload = JSON.parse(raw)
565
- const prompt = payload.prompt || ''
566
- if (!prompt.trim()) {
567
- writeJson(res, 400, { ok: false, error: 'Empty prompt' })
568
- return
569
- }
222
+ }, 'dsh-moa: plugin updater route')
223
+
224
+ // Route dispatch: req.method verified fail-closed in lib/routes.js
225
+ registerMoaRoutes(ctx, {
226
+ live,
227
+ callLlm,
228
+ liveCanvas,
229
+ getSettingsScope: () => settingsScope,
230
+ })
570
231
 
571
- const cfg = live()
572
- if (cfg.enabled === false) {
573
- writeJson(res, 400, { ok: false, error: 'MoA is disabled in settings' })
574
- return
575
- }
576
- const presetName = payload.preset || cfg.default_preset || 'default'
577
- const preset = (cfg.presets || []).find((p) => p.name === presetName) || cfg.presets?.[0]
578
-
579
- const result = await runMoAPipeline({
580
- userPrompt: prompt,
581
- messages: payload.messages || [],
582
- preset,
583
- callLlm,
584
- cwd: payload.cwd || process.cwd(),
585
- prices: cfg.prices || {},
586
- liveCanvas,
587
- })
588
-
589
- writeJson(res, 200, { ok: true, ...result })
590
- } catch (err) {
591
- writeJson(res, 500, { ok: false, error: err?.message || String(err) })
592
- }
593
- },
594
- })
595
- }, 'dsh-moa: run pipeline route')
596
232
  // Session turn interceptor: route turn to real aggregator model & stream MoA synthesis
597
233
  const moaPendingTurns = new WeakMap()
598
234
  const moaSessionTurns = new Map()
@@ -0,0 +1,225 @@
1
+ /**
2
+ * DeepSeek Harness Mixture of Agents (MoA) — Candidate Fan-Out & Retry
3
+ */
4
+
5
+ import { bestEffort } from './best-effort.js'
6
+ import { slotLabel, cleanAdvisoryMessages, ROLE_PERSONA_PROMPTS, SYSTEM_ROLE_PROPOSER } from './moa-prompts.js'
7
+ import { estimateTokenCost } from './pricing.js'
8
+
9
+ export const REFERENCE_SYSTEM_PROMPT = SYSTEM_ROLE_PROPOSER
10
+
11
+ export async function callWithTransientRetry(callLlmFn, callArgs, maxRetries = 0, retryDelayMs = 1200) {
12
+ let attempt = 0
13
+ while (true) {
14
+ if (callArgs?.signal?.aborted) throw new Error('Aborted')
15
+ try {
16
+ return await callLlmFn(callArgs)
17
+ } catch (err) {
18
+ attempt++
19
+ const msg = err?.message || String(err)
20
+ const isTransient = /429|rate limit|502|503|504|econnreset|etimedout|socket hang up/i.test(msg)
21
+ if (attempt <= maxRetries && isTransient && !callArgs?.signal?.aborted) {
22
+ await new Promise((r) => setTimeout(r, retryDelayMs))
23
+ if (callArgs?.signal?.aborted) throw new Error('Aborted')
24
+ continue
25
+ }
26
+ throw err
27
+ }
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Dispatches queries to all reference models in parallel with transient retry and quorum straggler mitigation.
33
+ */
34
+ export async function runReferencesParallel(references, messages, options = {}, callLlm, onProgress) {
35
+ if (!Array.isArray(references) || references.length === 0) {
36
+ return []
37
+ }
38
+
39
+ const advisoryMessages = cleanAdvisoryMessages(messages)
40
+ const systemPrompt = options.systemPrompt || REFERENCE_SYSTEM_PROMPT
41
+ const timeoutMs = options.timeoutMs ?? ((options.timeoutSec ?? 60) * 1000)
42
+ const maxRetries = options.maxRetries ?? 0
43
+ const quorumEnabled = Boolean(options.quorumEnabled)
44
+ const gracePeriodMs = (options.gracePeriodSec ?? 10) * 1000
45
+
46
+ const total = references.length
47
+ let finishedCount = 0
48
+ const results = new Array(total)
49
+ const abortControllers = references.map(() => new AbortController())
50
+ if (options.signal) {
51
+ if (options.signal.aborted) {
52
+ return references.map((r, i) => ({
53
+ index: i + 1,
54
+ provider: r.provider,
55
+ model: r.model,
56
+ label: slotLabel(r),
57
+ ok: false,
58
+ text: '(aborted)',
59
+ error: 'Turn aborted',
60
+ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
61
+ costUsd: 0,
62
+ }))
63
+ }
64
+ if (typeof options.signal.addEventListener === 'function') {
65
+ options.signal.addEventListener('abort', () => {
66
+ for (const ac of abortControllers) {
67
+ bestEffort('turn abort signal', () => ac.abort(new Error('Turn aborted')))
68
+ }
69
+ }, { once: true })
70
+ }
71
+ }
72
+
73
+ let onTaskFinished = null
74
+ const notifyFinished = () => {
75
+ if (typeof onTaskFinished === 'function') onTaskFinished()
76
+ }
77
+
78
+ if (typeof onProgress === 'function') {
79
+ onProgress(`⚡ *Launching ${total} candidate models in parallel...*\n`)
80
+ }
81
+
82
+ references.forEach((slot, i) => {
83
+ const label = slotLabel(slot)
84
+ const persona = slot?.role_persona && ROLE_PERSONA_PROMPTS[slot.role_persona]
85
+ ? `\n\n${ROLE_PERSONA_PROMPTS[slot.role_persona]}`
86
+ : ''
87
+ const slotMessages = [{ role: 'system', content: systemPrompt + persona }, ...advisoryMessages]
88
+
89
+ const runOne = async () => {
90
+ try {
91
+ const callPromise = callWithTransientRetry(
92
+ callLlm,
93
+ {
94
+ provider: slot.provider,
95
+ model: slot.model,
96
+ messages: slotMessages,
97
+ temperature: options.temperature ?? 0.6,
98
+ maxTokens: options.maxTokens ?? 4096,
99
+ timeoutMs,
100
+ signal: abortControllers[i].signal,
101
+ },
102
+ maxRetries
103
+ )
104
+
105
+ const timeoutPromise = new Promise((_, reject) => {
106
+ const timer = setTimeout(() => reject(new Error(`Timeout after ${Math.round(timeoutMs / 1000)}s`)), timeoutMs)
107
+ timer.unref?.()
108
+ abortControllers[i].signal.addEventListener('abort', () => clearTimeout(timer), { once: true })
109
+ })
110
+
111
+ const res = await Promise.race([callPromise, timeoutPromise])
112
+ const text = typeof res === 'string' ? res : (res?.content || res?.text || '')
113
+
114
+ const fallbackUsage = (typeof res === 'object' && res?.usage) ? res.usage : {
115
+ inputTokens: Math.max(1, Math.round(slotMessages.map((m) => m.content).join('').length / 4)),
116
+ outputTokens: Math.max(1, Math.round(text.length / 4)),
117
+ }
118
+ const costInfo = estimateTokenCost(slot, fallbackUsage, options.prices)
119
+
120
+ finishedCount++
121
+ if (typeof onProgress === 'function') {
122
+ const costStr = costInfo.costUsd > 0 ? ` (~\$${costInfo.costUsd.toFixed(4)})` : ''
123
+ onProgress(`✅ *Candidate ${i + 1}/${total} (${label}) finished${costStr}*\n`)
124
+ }
125
+
126
+ results[i] = {
127
+ index: i + 1,
128
+ slot,
129
+ label,
130
+ text,
131
+ usage: costInfo,
132
+ costUsd: costInfo.costUsd,
133
+ ok: true,
134
+ }
135
+ } catch (err) {
136
+ finishedCount++
137
+ const errMsg = err?.message || String(err)
138
+ console.warn(`[dsh-moa] Reference ${label} failed:`, errMsg)
139
+ if (typeof onProgress === 'function') {
140
+ onProgress(`⚠️ *Candidate ${i + 1}/${total} (${label}) error: ${errMsg}*\n`)
141
+ }
142
+ results[i] = {
143
+ index: i + 1,
144
+ slot,
145
+ label,
146
+ text: `[Model ${label} error: ${errMsg}]`,
147
+ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
148
+ costUsd: 0,
149
+ ok: false,
150
+ error: errMsg,
151
+ }
152
+ } finally {
153
+ notifyFinished()
154
+ }
155
+ }
156
+
157
+ runOne()
158
+ })
159
+
160
+ // Straggler mitigation via Quorum + Grace Period
161
+ if (quorumEnabled && total >= 3) {
162
+ const quorumTarget = Math.max(2, Math.min(total - 1, Math.ceil(total * 0.6)))
163
+ let graceTimer = null
164
+
165
+ await new Promise((resolve) => {
166
+ const checkQuorum = () => {
167
+ if (finishedCount >= total) {
168
+ if (graceTimer) clearTimeout(graceTimer)
169
+ resolve()
170
+ return
171
+ }
172
+
173
+ if (finishedCount >= quorumTarget && !graceTimer) {
174
+ if (typeof onProgress === 'function') {
175
+ onProgress(`⏳ *Quorum reached (${finishedCount}/${total}). Starting ${Math.round(gracePeriodMs / 1000)}s grace period for stragglers...*\n`)
176
+ }
177
+ graceTimer = setTimeout(() => {
178
+ if (typeof onProgress === 'function' && finishedCount < total) {
179
+ onProgress(`⏩ *Grace period expired. Proceeding with ${finishedCount}/${total} ready candidates.*\n`)
180
+ }
181
+ for (let k = 0; k < total; k++) {
182
+ if (!results[k]) {
183
+ bestEffort('quorum grace period abort', () => abortControllers[k].abort(new Error('Quorum grace period timed out')))
184
+ }
185
+ }
186
+ resolve()
187
+ }, gracePeriodMs)
188
+ graceTimer.unref?.()
189
+ }
190
+ }
191
+
192
+ onTaskFinished = checkQuorum
193
+ checkQuorum()
194
+ })
195
+
196
+ for (let i = 0; i < total; i++) {
197
+ if (!results[i]) {
198
+ const slot = references[i]
199
+ const label = slotLabel(slot)
200
+ results[i] = {
201
+ index: i + 1,
202
+ slot,
203
+ label,
204
+ text: `[Model ${label} timed out (quorum grace period exceeded)]`,
205
+ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
206
+ costUsd: 0,
207
+ ok: false,
208
+ error: 'Quorum grace period timed out',
209
+ }
210
+ }
211
+ }
212
+ return results
213
+ }
214
+
215
+ // Standard mode: wait for all tasks to complete
216
+ await new Promise((resolve) => {
217
+ const checkAll = () => {
218
+ if (finishedCount >= total) resolve()
219
+ }
220
+ onTaskFinished = checkAll
221
+ checkAll()
222
+ })
223
+
224
+ return results
225
+ }
package/lib/moa-parser.js CHANGED
@@ -138,6 +138,12 @@ export function formatMoAResponse({ moaResult, presetName }) {
138
138
  parts.push('> 🔄 **Mode**: Iterative project refinement (Refinement)')
139
139
  }
140
140
 
141
+ if (moaResult?.skippedFiles > 0) {
142
+ const preview = moaResult.skippedList?.slice(0, 3)?.join(', ') || ''
143
+ const more = (moaResult.skippedList?.length || 0) > 3 ? ` and ${(moaResult.skippedList?.length || 0) - 3} more` : ''
144
+ parts.push(`> ⚠️ **Workspace Notice**: ${moaResult.skippedFiles} file(s) skipped during workspace scan (${preview}${more}). Context representation may be partial.`)
145
+ }
146
+
141
147
  const hasPromoted = moaResult?.promotedFiles && moaResult.promotedFiles.length > 0
142
148
  if (hasPromoted) {
143
149
  parts.push(`> 📦 **Files created in the project**: \`${moaResult.promotedFiles.join('`, `')}\``)