@goodandready/dsh-moa 0.2.7 → 0.2.8

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/client.js CHANGED
@@ -650,7 +650,27 @@ window.__ModuleLoader__.load({
650
650
  placeholder: t('judgeCriteriaPlaceholder') || 'например: Приоритет — производительность, чистый код, минимум зависимостей...',
651
651
  value: judgeCriteria,
652
652
  onChange: (e) => setJudgeCriteria(e.target.value),
653
- })
653
+ }),
654
+ React.createElement(
655
+ 'div',
656
+ { style: { marginTop: 8, display: 'flex', flexDirection: 'column', gap: 4 } },
657
+ React.createElement(
658
+ 'label',
659
+ { style: { display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', fontSize: 13, fontWeight: 500 } },
660
+ React.createElement('input', {
661
+ type: 'checkbox',
662
+ checked: currentPreset.ask_clarifying_questions !== false,
663
+ onChange: (e) => updateCurrentPreset((p) => ({ ...p, ask_clarifying_questions: e.target.checked })),
664
+ style: { accentColor: 'var(--dsw-alias-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
665
+ }),
666
+ t('askQuestionsLabel') || 'Спрашивать уточняющие вопросы при неясных задачах (Опросник)'
667
+ ),
668
+ React.createElement(
669
+ 'div',
670
+ { className: 'moa-hint', style: { marginLeft: 22 } },
671
+ t('askQuestionsHint') || 'Если включено: при коротких задачах судья сформирует опросник. Если выключено: MoA сразу переходит к генерации решения.'
672
+ )
673
+ )
654
674
  )
655
675
  ),
656
676
  /* Proposers / Advisors Box with Temperature Slider */
@@ -92,7 +92,7 @@ function sanitizePath(filePath) {
92
92
  export async function collectProjectContext(baseDir, maxCharBudget = 16000) {
93
93
  if (!baseDir) return { files: [], totalChars: 0 }
94
94
 
95
- const ignoreDirs = new Set(['node_modules', '.git', '.moa', '.worktrees', 'dist', 'build', '.dsh'])
95
+ const ignoreDirs = new Set(['node_modules', '.git', '.moa', '.worktrees', 'dist', 'build', '.dsh', '.next', '.nuxt', '.cache', 'coverage', '.turbo', '.idea', '.vscode'])
96
96
  const codeExts = new Set([
97
97
  '.html', '.htm', '.css', '.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx',
98
98
  '.vue', '.svelte', '.json', '.py', '.md', '.txt', '.sh', '.yaml', '.yml',
package/lib/index.js CHANGED
@@ -44,6 +44,7 @@ export const ModelSlotSchema = z.object({
44
44
  export const PresetSchema = z.object({
45
45
  name: z.string(),
46
46
  enabled: z.boolean().default(true),
47
+ ask_clarifying_questions: z.boolean().default(true),
47
48
  reference_models: z.array(ModelSlotSchema).default([]),
48
49
  aggregator: ModelSlotSchema.default({ provider: 'codex', model: 'gpt-5.6-sol' }),
49
50
  reference_temperature: z.number().default(0.6),
@@ -61,6 +62,7 @@ export const Config = z.object({
61
62
  {
62
63
  name: 'default',
63
64
  enabled: true,
65
+ ask_clarifying_questions: true,
64
66
  reference_models: [
65
67
  { provider: 'opencode-go', model: 'deepseek-v4-flash' },
66
68
  { provider: 'grok', model: 'grok-build-0.1' },
@@ -75,6 +77,7 @@ export const Config = z.object({
75
77
  {
76
78
  name: 'fast',
77
79
  enabled: true,
80
+ ask_clarifying_questions: false,
78
81
  reference_models: [
79
82
  { provider: 'opencode-go', model: 'deepseek-v4-flash' },
80
83
  ],
@@ -88,6 +91,7 @@ export const Config = z.object({
88
91
  {
89
92
  name: 'deep-reasoning',
90
93
  enabled: true,
94
+ ask_clarifying_questions: true,
91
95
  reference_models: [
92
96
  { provider: 'grok', model: 'grok-build-0.1' },
93
97
  { provider: 'commandcode', model: 'deepseek/deepseek-v4-flash' },
@@ -449,11 +453,39 @@ export function apply(ctx, config) {
449
453
  },
450
454
  })
451
455
  }, 'dsh-moa: run pipeline route')
452
-
453
456
  // Session turn interceptor: route turn to real aggregator model & stream MoA synthesis
454
457
  const moaPendingTurns = new WeakMap()
455
458
  const moaSessionTurns = new Map()
456
459
 
460
+ function setSessionTurn(sessionId, turnContext) {
461
+ if (!sessionId) return
462
+ const existing = moaSessionTurns.get(sessionId)
463
+ if (existing?.ttlTimer) clearTimeout(existing.ttlTimer)
464
+
465
+ // TTL (3 minutes) to prevent memory leaks if turn is aborted or skipped before llm/stream
466
+ const ttlTimer = setTimeout(() => {
467
+ moaSessionTurns.delete(sessionId)
468
+ }, 180000)
469
+ ttlTimer.unref?.()
470
+
471
+ moaSessionTurns.set(sessionId, { ...turnContext, ttlTimer })
472
+ }
473
+
474
+ function getSessionTurn(sessionId) {
475
+ if (!sessionId) return null
476
+ return moaSessionTurns.get(sessionId) || null
477
+ }
478
+
479
+ function consumeSessionTurn(sessionId) {
480
+ if (!sessionId) return null
481
+ const entry = moaSessionTurns.get(sessionId)
482
+ if (entry) {
483
+ if (entry.ttlTimer) clearTimeout(entry.ttlTimer)
484
+ moaSessionTurns.delete(sessionId)
485
+ }
486
+ return entry
487
+ }
488
+
457
489
  ctx.on('agent/pre-step', async (event, next) => {
458
490
  const messages = event.messages || []
459
491
  const lastUserMsg = [...messages].reverse().find((m) => m && m.role === 'user')
@@ -485,7 +517,7 @@ export function apply(ctx, config) {
485
517
  moaPendingTurns.set(event.signal, turnContext)
486
518
  }
487
519
  if (event.session?.id) {
488
- moaSessionTurns.set(event.session.id, turnContext)
520
+ setSessionTurn(event.session.id, turnContext)
489
521
  }
490
522
  }
491
523
  }
@@ -496,7 +528,7 @@ export function apply(ctx, config) {
496
528
  ctx.on('agent/request', async (payload, next) => {
497
529
  const resolved = await next()
498
530
  const turnContext = (payload.signal ? moaPendingTurns.get(payload.signal) : null)
499
- || (payload.session?.id ? moaSessionTurns.get(payload.session.id) : null)
531
+ || (payload.session?.id ? getSessionTurn(payload.session.id) : null)
500
532
 
501
533
  if (turnContext) {
502
534
  return {
@@ -511,11 +543,11 @@ export function apply(ctx, config) {
511
543
  ctx.effect(() => {
512
544
  return ctx.on('llm/stream', (options, next) => {
513
545
  const turnContext = (options.signal ? moaPendingTurns.get(options.signal) : null)
514
- || (options.session?.id ? moaSessionTurns.get(options.session.id) : null)
546
+ || (options.session?.id ? consumeSessionTurn(options.session.id) : null)
515
547
 
516
548
  if (turnContext) {
517
549
  if (options.signal) moaPendingTurns.delete(options.signal)
518
- if (options.session?.id) moaSessionTurns.delete(options.session.id)
550
+ if (options.session?.id) consumeSessionTurn(options.session.id)
519
551
 
520
552
  return streamMoATurn(turnContext, options)
521
553
  }
package/lib/moa-runner.js CHANGED
@@ -383,7 +383,8 @@ export async function runMoAPipeline({
383
383
  }
384
384
 
385
385
  // Phase 1: Check if clarifying questions should be asked
386
- const needsQuestions = !skipQuestions && !isRefinement && isBroadPromptRequiringQuestions(userPrompt, messages)
386
+ const allowQuestions = preset?.ask_clarifying_questions !== false && preset?.askClarifyingQuestions !== false
387
+ const needsQuestions = allowQuestions && !skipQuestions && !isRefinement && isBroadPromptRequiringQuestions(userPrompt, messages)
387
388
  if (needsQuestions) {
388
389
  if (typeof onProgress === 'function') {
389
390
  onProgress('🔍 *Задача общего характера. Советники формируют ключевые развилки...*\n\n')
@@ -446,6 +447,32 @@ export async function runMoAPipeline({
446
447
  onProgress,
447
448
  )
448
449
 
450
+ // Fast-fail: If all candidates failed, do not call judge to save tokens and avoid synthesized hallucinations
451
+ const allFailed = referenceOutputs.length > 0 && referenceOutputs.every((r) => !r.ok)
452
+ if (allFailed) {
453
+ const errorDetails = referenceOutputs
454
+ .map((r, i) => `• Кандидат ${i + 1} (${r.label}): ${r.text}`)
455
+ .join('\n')
456
+ const failMessage = `⚠️ **Все модели-советники (${referenceOutputs.length}) завершились с ошибкой**.\n\nСудья не вызывался для предотвращения бессмысленного расхода токенов.\n\n### Детали ошибок:\n${errorDetails}`
457
+
458
+ await cleanMoaWorkspaces(workDir)
459
+
460
+ return {
461
+ kind: 'failure',
462
+ content: failMessage,
463
+ aggregator: slotLabel(aggregator),
464
+ references: referenceOutputs,
465
+ presetName: preset?.name || 'default',
466
+ promotedFiles: [],
467
+ usage: {
468
+ totalTokens: 0,
469
+ totalCostUsd: 0,
470
+ candidates: referenceOutputs.map((r) => ({ label: r.label, usage: r.usage, costUsd: 0 })),
471
+ aggregator: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },
472
+ },
473
+ durationMs: Date.now() - startTime,
474
+ }
475
+ }
449
476
  // Write files for each candidate into .moa/candidate-X/
450
477
  for (let i = 0; i < referenceOutputs.length; i++) {
451
478
  const ref = referenceOutputs[i]
@@ -691,73 +718,93 @@ export async function* streamMoATurn({ targetPreset, userPrompt, messages, callL
691
718
  yield { type: 'block-start', index: 0, blockType: 'text' }
692
719
  yield { type: 'text-delta', index: 0, text: '🧠 *Mixture of Agents запущен...*\n\n' }
693
720
 
694
- // Queue for streaming progress updates from within runMoAPipeline
695
- const progressChunks = []
696
- const onProgress = (text) => {
697
- progressChunks.push(text)
721
+ // Async push-queue for zero-latency live delta streaming
722
+ const queue = []
723
+ let notify = null
724
+
725
+ const pushUpdate = (text) => {
726
+ queue.push({ type: 'text-delta', index: 0, text })
727
+ if (notify) {
728
+ notify()
729
+ notify = null
730
+ }
698
731
  }
699
732
 
700
- try {
701
- const pipelinePromise = runMoAPipeline({
702
- userPrompt,
703
- messages,
704
- preset: targetPreset,
705
- callLlm,
706
- cwd,
707
- onProgress,
733
+ let done = false
734
+ const pipelinePromise = runMoAPipeline({
735
+ userPrompt,
736
+ messages,
737
+ preset: targetPreset,
738
+ callLlm,
739
+ cwd,
740
+ onProgress: pushUpdate,
741
+ })
742
+ .catch((err) => ({ error: err }))
743
+ .finally(() => {
744
+ done = true
745
+ if (notify) {
746
+ notify()
747
+ notify = null
748
+ }
708
749
  })
709
750
 
710
- const startTime = Date.now()
711
- let lastYieldTime = Date.now()
751
+ const startTime = Date.now()
752
+ let lastYieldTime = Date.now()
712
753
 
713
- // Yield progressive updates while pipeline runs
714
- while (true) {
715
- if (signal?.aborted) return
754
+ try {
755
+ while (!done || queue.length > 0) {
756
+ if (signal?.aborted) {
757
+ await cleanMoaWorkspaces(cwd)
758
+ return
759
+ }
716
760
 
717
- let emittedAny = false
718
- while (progressChunks.length > 0) {
719
- const chunk = progressChunks.shift()
720
- yield { type: 'text-delta', index: 0, text: chunk }
721
- emittedAny = true
761
+ while (queue.length > 0) {
762
+ const item = queue.shift()
763
+ yield item
722
764
  lastYieldTime = Date.now()
723
765
  }
724
766
 
725
- const done = await Promise.race([
726
- pipelinePromise.then(() => true),
727
- new Promise((r) => { const t = setTimeout(() => r(false), 500); t.unref?.(); }),
728
- ])
729
767
  if (done) break
730
768
 
769
+ // Wait for next push item or max 2.5s heartbeat
770
+ await Promise.race([
771
+ new Promise((resolve) => { notify = resolve }),
772
+ new Promise((resolve) => { const t = setTimeout(resolve, 2500); t.unref?.(); }),
773
+ ])
774
+
731
775
  const elapsedSec = Math.floor((Date.now() - startTime) / 1000)
732
- // If 3 seconds passed without any progress message, send a live tick
733
- if (!emittedAny && Date.now() - lastYieldTime >= 3000) {
776
+ if (!done && Date.now() - lastYieldTime >= 3000) {
734
777
  yield { type: 'text-delta', index: 0, text: `⏳ *[${elapsedSec}с] Идёт обработка...*\n` }
735
778
  lastYieldTime = Date.now()
736
779
  }
737
780
  }
738
781
 
739
- while (progressChunks.length > 0) {
740
- const chunk = progressChunks.shift()
741
- yield { type: 'text-delta', index: 0, text: chunk }
782
+ const result = await pipelinePromise
783
+ if (signal?.aborted) {
784
+ await cleanMoaWorkspaces(cwd)
785
+ return
742
786
  }
743
787
 
744
- const moaResult = await pipelinePromise
745
- if (signal?.aborted) return
788
+ if (result?.error) {
789
+ const errText = '\n\n⚠️ **Ошибка Mixture of Agents**: ' + (result.error?.message || String(result.error))
790
+ yield { type: 'text-delta', index: 0, text: errText }
791
+ yield { type: 'block-end', index: 0, block: { type: 'text', text: errText } }
792
+ yield { type: 'finish', reason: { kind: 'stop' } }
793
+ return
794
+ }
746
795
 
747
796
  const formatted = formatMoAResponse({
748
- moaResult,
797
+ moaResult: result,
749
798
  presetName: targetPreset?.name || 'default',
750
799
  })
751
800
 
752
801
  yield { type: 'text-delta', index: 0, text: '\n---\n\n' + formatted }
753
802
  yield { type: 'block-end', index: 0, block: { type: 'text', text: formatted } }
754
- yield { type: 'usage', usage: { inputTokens: moaResult?.usage?.totalTokens || 100, outputTokens: formatted.length } }
755
- yield { type: 'finish', reason: { kind: 'stop' } }
756
- } catch (err) {
757
- if (signal?.aborted) return
758
- const errText = '\n\n⚠️ **Ошибка Mixture of Agents**: ' + (err?.message || String(err))
759
- yield { type: 'text-delta', index: 0, text: errText }
760
- yield { type: 'block-end', index: 0, block: { type: 'text', text: errText } }
803
+ yield { type: 'usage', usage: { inputTokens: result?.usage?.totalTokens || 100, outputTokens: formatted.length } }
761
804
  yield { type: 'finish', reason: { kind: 'stop' } }
805
+ } finally {
806
+ if (signal?.aborted) {
807
+ await cleanMoaWorkspaces(cwd)
808
+ }
762
809
  }
763
810
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-moa",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "Mixture of Agents (MoA) plugin for DeepSeek Harness with /moa slash command",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",