@goodandready/dsh-moa 0.2.31 → 0.2.33

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
@@ -422,20 +422,19 @@ window.__ModuleLoader__.load({
422
422
  }
423
423
 
424
424
  const PROVIDER_TITLES = {
425
- codex: 'ChatGPT Codex',
425
+ deepseek: 'DeepSeek',
426
+ openai: 'OpenAI',
427
+ anthropic: 'Anthropic Claude',
426
428
  claude: 'Claude (Anthropic)',
427
- grok: 'Grok (xAI)',
428
- antigravity: 'Antigravity (Google)',
429
+ google: 'Google Gemini',
429
430
  gemini: 'Google Gemini',
430
- commandcode: 'CommandCode Provider',
431
- 'opencode-go': 'OpenCode-Go',
432
- 'qwen-token-plan': 'Qwen (Token Plan)',
433
431
  groq: 'Groq Cloud',
434
- zai: 'Zhipu GLM',
435
432
  ollama: 'Ollama (Local / Cloud)',
436
- 'kimi-coding': 'Kimi Coding',
433
+ openrouter: 'OpenRouter',
434
+ qwen: 'Qwen',
435
+ zai: 'Zhipu GLM',
437
436
  minimax: 'MiniMax',
438
- 'xiaomi-token-plan-sgp': 'Xiaomi Token Plan',
437
+ mistral: 'Mistral AI',
439
438
  'deepseek-official': 'DeepSeek Official',
440
439
  }
441
440
 
@@ -850,7 +849,7 @@ window.__ModuleLoader__.load({
850
849
  const currentPreset = presets.find((p) => p.name === selectedPresetName) || presets[0] || {
851
850
  name: 'default',
852
851
  reference_models: [],
853
- aggregator: { provider: 'codex', model: 'gpt-5.6-sol' },
852
+ aggregator: { provider: (availableModels[0] && availableModels[0].provider) || '', model: (availableModels[0] && availableModels[0].model) || '' },
854
853
  aggregator_temperature: 0.4,
855
854
  reference_temperature: 0.6,
856
855
  judge_criteria: '',
@@ -902,8 +901,8 @@ window.__ModuleLoader__.load({
902
901
  } else {
903
902
  while (refs.length < targetCount) {
904
903
  const fallback = availableModels[refs.length % (availableModels.length || 1)] || {
905
- provider: 'opencode-go',
906
- model: 'deepseek-v4-flash',
904
+ provider: '',
905
+ model: '',
907
906
  }
908
907
  refs.push({ provider: fallback.provider, model: fallback.model })
909
908
  }
@@ -924,8 +923,8 @@ window.__ModuleLoader__.load({
924
923
  updateCurrentPreset((p) => {
925
924
  const refs = [...(p.reference_models || [])]
926
925
  const fallback = availableModels[refs.length % (availableModels.length || 1)] || {
927
- provider: 'opencode-go',
928
- model: 'deepseek-v4-flash',
926
+ provider: '',
927
+ model: '',
929
928
  }
930
929
  refs.push({ provider: fallback.provider, model: fallback.model, role_persona: 'general' })
931
930
  return { ...p, reference_models: refs }
@@ -945,13 +944,18 @@ window.__ModuleLoader__.load({
945
944
  name,
946
945
  enabled: true,
947
946
  ask_clarifying_questions: true,
948
- reference_models: currentPreset?.reference_models?.length ? structuredClone(currentPreset.reference_models) : [
949
- { provider: 'opencode-go', model: 'deepseek-v4-flash', role_persona: 'general' },
950
- { provider: 'codex', model: 'gpt-5.6-sol', role_persona: 'general' },
951
- ],
947
+ reference_models: currentPreset?.reference_models?.length ? structuredClone(currentPreset.reference_models) : (
948
+ availableModels.length >= 2 ? [
949
+ { provider: availableModels[0].provider, model: availableModels[0].model, role_persona: 'general' },
950
+ { provider: availableModels[1].provider, model: availableModels[1].model, role_persona: 'general' },
951
+ ] : [
952
+ { provider: '', model: '', role_persona: 'general' },
953
+ { provider: '', model: '', role_persona: 'general' },
954
+ ]
955
+ ),
952
956
  aggregator: currentPreset?.aggregator?.provider ? structuredClone(currentPreset.aggregator) : {
953
- provider: 'codex',
954
- model: 'gpt-5.6-sol',
957
+ provider: (availableModels[0] && availableModels[0].provider) || '',
958
+ model: (availableModels[0] && availableModels[0].model) || '',
955
959
  },
956
960
  reference_temperature: 0.6,
957
961
  aggregator_temperature: 0.4,
@@ -1513,15 +1517,16 @@ window.__ModuleLoader__.load({
1513
1517
  'div',
1514
1518
  { style: { flex: 1 } },
1515
1519
  React.createElement(SearchableModelPicker, {
1516
- value: fb,
1517
- onChange: (val) => {
1520
+ provider: (fb && fb.provider) || '',
1521
+ model: (fb && fb.model) || '',
1522
+ onChange: (prov, mod) => {
1518
1523
  updateCurrentPreset((p) => {
1519
1524
  const list = [...(p.aggregator_fallbacks || [])]
1520
- list[fbIdx] = val
1525
+ list[fbIdx] = { provider: prov, model: mod }
1521
1526
  return { ...p, aggregator_fallbacks: list }
1522
1527
  })
1523
1528
  },
1524
- allModels,
1529
+ availableModels,
1525
1530
  t,
1526
1531
  })
1527
1532
  ),
@@ -1553,7 +1558,7 @@ window.__ModuleLoader__.load({
1553
1558
  onClick: () => {
1554
1559
  updateCurrentPreset((p) => ({
1555
1560
  ...p,
1556
- aggregator_fallbacks: [...(p.aggregator_fallbacks || []), { provider: 'opencode-go', model: 'deepseek-v4-flash' }],
1561
+ aggregator_fallbacks: [...(p.aggregator_fallbacks || []), { provider: (availableModels[0] && availableModels[0].provider) || '', model: (availableModels[0] && availableModels[0].model) || '' }],
1557
1562
  }))
1558
1563
  },
1559
1564
  },
package/lib/history.js CHANGED
@@ -25,7 +25,8 @@ export function getDefaultHistoryDir() {
25
25
  return process.env.DSH_HISTORY_DIR
26
26
  }
27
27
  if (process.env.DSH_HOME) {
28
- return path.join(process.env.DSH_HOME, '.dsh')
28
+ const trimmed = process.env.DSH_HOME.trim()
29
+ return trimmed.endsWith('.dsh') ? trimmed : path.join(trimmed, '.dsh')
29
30
  }
30
31
  if (isTestEnvironment()) {
31
32
  return path.join(os.tmpdir(), '.dsh-test')
@@ -40,8 +41,6 @@ export function getDefaultHistoryFile() {
40
41
  return path.join(getDefaultHistoryDir(), 'moa-history.jsonl')
41
42
  }
42
43
 
43
- export const DEFAULT_HISTORY_DIR = getDefaultHistoryDir()
44
- export const DEFAULT_HISTORY_FILE = getDefaultHistoryFile()
45
44
 
46
45
  /** Max history file size before rotation (10 MB) */
47
46
  export const MAX_HISTORY_BYTES = 10 * 1024 * 1024
@@ -53,13 +52,6 @@ export function invalidateHistoryCache() {
53
52
  _historyCache = null
54
53
  }
55
54
 
56
- export function getHistoryFilePath(customDir) {
57
- if (customDir) {
58
- return path.join(customDir, '.moa-history.jsonl')
59
- }
60
- return getDefaultHistoryFile()
61
- }
62
-
63
55
  /**
64
56
  * Checks if the history file exceeds the size threshold and rotates it.
65
57
  * Moves current file to `${filePath}.1`, removing any older `.1` backup.
@@ -71,7 +63,7 @@ export function rotateHistoryFileIfNeeded(filePath = getDefaultHistoryFile(), ma
71
63
  if (stat.size >= maxBytes) {
72
64
  const backupPath = `${filePath}.1`
73
65
  if (fs.existsSync(backupPath)) {
74
- try { fs.unlinkSync(backupPath) } catch (unlinkErr) { /* backup unlink best-effort */ }
66
+ try { fs.unlinkSync(backupPath) } catch { /* backup unlink best-effort */ }
75
67
  }
76
68
  fs.renameSync(filePath, backupPath)
77
69
  invalidateHistoryCache()
@@ -383,7 +375,7 @@ export function getMoaRunById(runId, filePath = getDefaultHistoryFile()) {
383
375
  try {
384
376
  const item = JSON.parse(lines[i])
385
377
  if (item && item.id === runId) return item
386
- } catch (parseErr) { /* corrupt line skipped */ }
378
+ } catch { /* corrupt line skipped */ }
387
379
  }
388
380
  return null
389
381
  } catch {
@@ -412,7 +404,7 @@ export function exportMoaHistory(filePath = getDefaultHistoryFile(), options = {
412
404
  if (!item) continue
413
405
  if (presetFilter && presetFilter !== 'all' && item.preset !== presetFilter) continue
414
406
  records.push(item)
415
- } catch (parseErr) { /* corrupt line skipped */ }
407
+ } catch { /* corrupt line skipped */ }
416
408
  }
417
409
 
418
410
  if (format === 'csv') {
package/lib/index.js CHANGED
@@ -25,10 +25,8 @@ try {
25
25
  // safe fallback
26
26
  }
27
27
  import path from 'node:path'
28
- import os from 'node:os'
29
28
  import {
30
29
  parseMoACommand,
31
- runMoAPipeline,
32
30
  streamMoATurn,
33
31
  } from './moa-runner.js'
34
32
 
@@ -36,7 +34,7 @@ import { registerMoaRoutes } from './routes.js'
36
34
  import { resolvePresetForPrompt } from './moa-router.js'
37
35
 
38
36
  import { createLiveCanvasClient } from './live-canvas.js'
39
- import { registerPluginUpdater, isSafeWriteRequest } from './updater.js'
37
+ import { registerPluginUpdater } from './updater.js'
40
38
 
41
39
  export const name = '@goodandready/dsh-moa'
42
40
  export { collectProjectContext, formatProjectContext } from './file-workspace.js'
@@ -313,6 +311,15 @@ export function apply(ctx, config) {
313
311
  })
314
312
 
315
313
  // Session turn interceptor: route turn to real aggregator model & stream MoA synthesis
314
+
315
+ ctx.effect(() => {
316
+ return () => {
317
+ for (const entry of moaSessionTurns.values()) {
318
+ if (entry?.ttlTimer) clearTimeout(entry.ttlTimer)
319
+ }
320
+ moaSessionTurns.clear()
321
+ }
322
+ }, 'dsh-moa: session turns cleanup')
316
323
  const moaPendingTurns = new WeakMap()
317
324
  const moaSessionTurns = new Map()
318
325
 
@@ -345,7 +352,8 @@ export function apply(ctx, config) {
345
352
  return entry
346
353
  }
347
354
 
348
- ctx.on('agent/pre-step', async (event, next) => {
355
+ ctx.effect(() => {
356
+ return ctx.on('agent/pre-step', async (event, next) => {
349
357
  const messages = event.messages || []
350
358
  const lastUserMsg = [...messages].reverse().find((m) => m && m.role === 'user')
351
359
  const userText = typeof lastUserMsg?.content === 'string'
@@ -404,9 +412,11 @@ export function apply(ctx, config) {
404
412
  }
405
413
 
406
414
  return next()
407
- })
415
+ })
416
+ }, 'dsh-moa: agent pre-step interceptor')
408
417
 
409
- ctx.on('agent/request', async (payload, next) => {
418
+ ctx.effect(() => {
419
+ return ctx.on('agent/request', async (payload, next) => {
410
420
  const resolved = await next()
411
421
  const turnContext = (payload.signal ? moaPendingTurns.get(payload.signal) : null)
412
422
  || (payload.session?.id ? getSessionTurn(payload.session.id) : null)
@@ -419,7 +429,8 @@ export function apply(ctx, config) {
419
429
  }
420
430
  }
421
431
  return resolved
422
- })
432
+ })
433
+ }, 'dsh-moa: agent request interceptor')
423
434
 
424
435
  ctx.effect(() => {
425
436
  return ctx.on('llm/stream', (options, next) => {
@@ -46,19 +46,25 @@ export function pruneMultiTurnMessages(messages = [], maxHistoryTokens = 3000) {
46
46
  const nonSys = messages.filter((m) => m && m.role !== 'system')
47
47
  if (nonSys.length === 0) return pruned
48
48
 
49
- // Always keep the final 2 turns (last user and prior assistant)
49
+ // Always keep up to the final 4 turns (last 2 user + assistant pairs)
50
50
  const recentTurns = nonSys.slice(-4)
51
51
 
52
+ // Calculate dynamic character budget derived from maxHistoryTokens (approx 4 chars per token)
53
+ const totalCharBudget = Math.max(1200, (Number(maxHistoryTokens) || 3000) * 4)
54
+ const maxTurnChars = Math.max(400, Math.floor(totalCharBudget / recentTurns.length))
55
+ const maxHead = Math.floor(maxTurnChars * 0.75)
56
+ const maxTail = Math.max(50, maxTurnChars - maxHead)
57
+
52
58
  for (let idx = 0; idx < recentTurns.length; idx++) {
53
59
  const msg = recentTurns[idx]
54
60
  const isLatest = idx === recentTurns.length - 1
55
61
  const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content)
56
62
 
57
- if (isLatest || content.length < 800) {
63
+ if (isLatest || content.length <= maxTurnChars) {
58
64
  pruned.push(msg)
59
65
  } else {
60
66
  // Prune long prior assistant or user text to protect candidate context
61
- const truncated = content.slice(0, 750) + '\n\n[...context pruned for token efficiency...]\n' + content.slice(-250)
67
+ const truncated = content.slice(0, maxHead) + '\n\n[...context pruned for token efficiency...]\n' + content.slice(-maxTail)
62
68
  pruned.push({
63
69
  role: msg.role,
64
70
  content: truncated,
package/lib/moa-runner.js CHANGED
@@ -1,15 +1,13 @@
1
1
  import { logger } from './logger.js'
2
- import path from 'node:path'
3
2
  import crypto from 'node:crypto'
4
- import { bestEffort } from './best-effort.js'
5
3
  import { extractFileBlocks, collectProjectContext, formatProjectContext, isRefinementTask, writeCandidateWorkspace, promoteCandidateWorkspace, cleanMoaWorkspaces, verifyFileSyntax } from './file-workspace.js'
6
4
  import { estimateTokenCost, summarizeMoAUsage } from './pricing.js'
7
5
  import { recordMoaRunAsync, candidatesForHistory, isTestEnvironment } from './history.js'
8
6
  import { createPromotedPreview } from './live-canvas.js'
9
- import { slotLabel, cleanAdvisoryMessages, isBroadPromptRequiringQuestions, buildQuestionSynthesisPrompt, buildCuratorSynthesisPrompt, buildSynthesisPrompt, buildPeerCritiquePrompt, ROLE_PERSONA_PROMPTS, SYSTEM_ROLE_PROPOSER, ANTIPATTERNS_RUBRIC } from './moa-prompts.js'
10
- import { parseWinnerIndex, parseRecommendedAssembler, parseMoACommand, stripOrSummarizeCode, formatMoAResponse } from './moa-parser.js'
7
+ import { slotLabel, isBroadPromptRequiringQuestions, buildQuestionSynthesisPrompt, buildSynthesisPrompt, buildPeerCritiquePrompt } from './moa-prompts.js'
8
+ import { parseWinnerIndex, parseRecommendedAssembler } from './moa-parser.js'
11
9
  import { REFERENCE_SYSTEM_PROMPT, callWithTransientRetry, runReferencesParallel } from './moa-candidates.js'
12
- import { executeTestGateForCandidates, runCandidateTestGate } from './moa-test-gate.js'
10
+ import { executeTestGateForCandidates } from './moa-test-gate.js'
13
11
  import { executeMultiJudgePanel, buildCompositeBlockDirectives } from './moa-multi-judge.js'
14
12
  import { applyBudgetGuardrails } from './moa-budget.js'
15
13
  import { extractPriorTurnBaseline, pruneMultiTurnMessages } from './moa-context.js'
package/lib/moa-stream.js CHANGED
@@ -109,8 +109,8 @@ export async function* streamMoATurn({ targetPreset, userPrompt, messages, callL
109
109
  yield {
110
110
  type: 'usage',
111
111
  usage: {
112
- inputTokens: result?.usage?.totalTokens || 0,
113
- outputTokens: Math.round((formatted.length || 0) / 4),
112
+ inputTokens: result?.usage?.totalInputTokens ?? (result?.usage?.totalTokens || 0),
113
+ outputTokens: result?.usage?.totalOutputTokens ?? Math.round((formatted.length || 0) / 4),
114
114
  },
115
115
  }
116
116
  yield { type: 'finish', reason: { kind: 'stop' } }
package/lib/pricing.js CHANGED
@@ -20,7 +20,7 @@ export function getDefaultCacheFile() {
20
20
  return join(process.env.DSH_STORAGE_DIR, 'dsh-moa-catalog.json')
21
21
  }
22
22
  const baseDir = process.env.DSH_HOME
23
- ? join(process.env.DSH_HOME, '.dsh')
23
+ ? (process.env.DSH_HOME.trim().endsWith('.dsh') ? process.env.DSH_HOME.trim() : join(process.env.DSH_HOME.trim(), '.dsh'))
24
24
  : join(homedir(), '.dsh')
25
25
  return join(baseDir, 'storages', 'dsh-moa-catalog.json')
26
26
  }
@@ -315,8 +315,10 @@ export function estimateTokenCost(slot = {}, usage = {}, customPrices = {}, cach
315
315
 
316
316
  export function summarizeMoAUsage(referenceOutputs = [], aggUsage = null) {
317
317
  const normAgg = aggUsage || { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 }
318
- const totalTokens = (referenceOutputs || []).reduce((acc, r) => acc + (r.usage?.totalTokens || 0), 0) + normAgg.totalTokens
318
+ const totalInputTokens = (referenceOutputs || []).reduce((acc, r) => acc + (r.usage?.inputTokens || r.usage?.promptTokens || 0), 0) + (normAgg.inputTokens || normAgg.promptTokens || 0)
319
+ const totalOutputTokens = (referenceOutputs || []).reduce((acc, r) => acc + (r.usage?.outputTokens || r.usage?.completionTokens || 0), 0) + (normAgg.outputTokens || normAgg.completionTokens || 0)
320
+ const totalTokens = (referenceOutputs || []).reduce((acc, r) => acc + (r.usage?.totalTokens || 0), 0) + (normAgg.totalTokens || (totalInputTokens + totalOutputTokens))
319
321
  const totalCostUsd = Number(((referenceOutputs || []).reduce((acc, r) => acc + (r.costUsd || 0), 0) + normAgg.costUsd).toFixed(5))
320
322
  const candidates = (referenceOutputs || []).map((r) => ({ label: r.label, usage: r.usage, costUsd: r.costUsd }))
321
- return { totalTokens, totalCostUsd, candidates, aggregator: normAgg }
323
+ return { totalTokens, totalInputTokens, totalOutputTokens, totalCostUsd, candidates, aggregator: normAgg }
322
324
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-moa",
3
- "version": "0.2.31",
3
+ "version": "0.2.33",
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",
@@ -54,10 +54,10 @@
54
54
  },
55
55
  "peerDependencies": {
56
56
  "@deepseek-ai/cordis": "^4.0.1",
57
- "@deepseek-ai/dsh-credentials": "^0.1.0-rc.6",
58
- "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
59
- "@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
60
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
57
+ "@deepseek-ai/dsh-credentials": "^0.1.7-rc.2 || ^0.2.0-rc.1",
58
+ "@deepseek-ai/dsh-host-webserver": "^0.1.7-rc.2 || ^0.2.0-rc.1",
59
+ "@deepseek-ai/dsh-settings": "^0.1.7-rc.2 || ^0.2.0-rc.1",
60
+ "@deepseek-ai/dsh-tools": "^0.1.7-rc.2 || ^0.2.0-rc.1",
61
61
  "@deepseek-ai/schemastery": "^3.18.1"
62
62
  },
63
63
  "peerDependenciesMeta": {