@goodandready/dsh-moa 0.2.38 → 0.2.39
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/README.md +1 -1
- package/README.ru.md +1 -1
- package/README.zh.md +1 -1
- package/lib/index.js +26 -19
- package/lib/moa-budget.js +65 -8
- package/lib/moa-parser.js +16 -3
- package/lib/moa-runner.js +11 -20
- package/lib/routes.js +13 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -140,7 +140,7 @@ Configure your models in `Settings → Plugins → Mixture of Agents`:
|
|
|
140
140
|
Inspect line-by-line differences between candidate proposals and the curator's synthesized deliverable directly in the UI. Features file selection, delta line highlights (added, removed, same), and unified diff rendering via a zero-dependency in-memory LCS algorithm.
|
|
141
141
|
|
|
142
142
|
### 8. Pre-Promotion Git Checkpoints (`dsh-time-machine`)
|
|
143
|
-
Before promoting any winning candidate files over the workspace root, `dsh-moa` invokes the local `dsh-time-machine` service to create a shadow Git checkpoint (`moa-pre-promotion: candidate-N`). If `dsh-time-machine` is absent or unreachable, file promotion
|
|
143
|
+
Before promoting any winning candidate files over the workspace root, `dsh-moa` invokes the local `dsh-time-machine` service to create a shadow Git checkpoint (`moa-pre-promotion: candidate-N`). If `dsh-time-machine` is absent or unreachable, file promotion is blocked (fail-closed) to protect uncheckpointed workspace files against unintended overwrites (override with `force: true` only when automated checkpoints are explicitly unneeded).
|
|
144
144
|
|
|
145
145
|
### 9. Automated Test Execution Gate
|
|
146
146
|
When `test_gate_enabled: true` and `test_command` (e.g. `npm test` or `pytest`) are configured, candidate code is executed in an ephemeral sandbox overlay. The judge receives concrete test outcomes, durations, and output logs to ground decisions in objective verification.
|
package/README.ru.md
CHANGED
|
@@ -140,7 +140,7 @@ graph TD
|
|
|
140
140
|
Позволяет визуально сопоставлять построчные различия между решениями моделей-советников и итоговым синтезом судьи прямо в интерфейсе. Поддерживает выбор конкретного файла, подсветку добавленных/удаленных строк и вычисление дельт с помощью легковесного алгоритма LCS без внешних зависимостей.
|
|
141
141
|
|
|
142
142
|
### 8. Теневые Git-чекпоинты (`dsh-time-machine`)
|
|
143
|
-
Перед промоушном файлов победителя в корень рабочей области `dsh-moa` автоматически обращается к сервису `dsh-time-machine` для создания снимка (`moa-pre-promotion: candidate-N`). Если плагин
|
|
143
|
+
Перед промоушном файлов победителя в корень рабочей области `dsh-moa` автоматически обращается к сервису `dsh-time-machine` для создания снимка (`moa-pre-promotion: candidate-N`). Если плагин недоступен или создание снимка завершилось ошибкой, перезапись файлов блокируется во избежание потери несохранённых данных (переопределяется параметром `force: true`, только если создание снимков намеренно отключено).
|
|
144
144
|
|
|
145
145
|
### 9. Автоматический шлюз выполнения тестов (Automated Test Execution Gate)
|
|
146
146
|
При включении `test_gate_enabled: true` и указании тестовой команды (`test_command`, например `npm test` или `pytest`) каждый кандидат запускается в изолированном оверлейном окружении. Судья получает объективный вердикт с кодами возврата, временем исполнения и логами тестов до вынесения итогового решения.
|
package/README.zh.md
CHANGED
|
@@ -140,7 +140,7 @@ graph TD
|
|
|
140
140
|
直接在界面中可视化对比顾问方案之间以及顾问与裁判综合方案之间的逐行差异。支持文件筛选、增删改高亮,基于纯 JavaScript LCS 差异算法,零第三方依赖。
|
|
141
141
|
|
|
142
142
|
### 8. 提升前 Git 快照检查点 (`dsh-time-machine`)
|
|
143
|
-
在将胜出候选者的文件提升并覆盖至项目根目录前,`dsh-moa` 自动调用本地 `dsh-time-machine` 建立影子 Git 检查点 (`moa-pre-promotion: candidate-N`)
|
|
143
|
+
在将胜出候选者的文件提升并覆盖至项目根目录前,`dsh-moa` 自动调用本地 `dsh-time-machine` 建立影子 Git 检查点 (`moa-pre-promotion: candidate-N`)。如 `dsh-time-machine` 未安装或无法访问,文件提升将被阻断以防止未备份的项目文件被意外覆盖(仅在明确不需要检查点时可通过 `force: true` 覆盖)。
|
|
144
144
|
|
|
145
145
|
### 18. 平滑降级与本地容灾恢复 (Local Fallback Resilience)
|
|
146
146
|
当 `local_fallback_enabled: true` 时,因网络中断、超时或速率限制(429/500)而失败的候选模型会自动使用配置的本地模型(Ollama / MiniPC)恢复。每个后备模型都拥有独立的超时执行配额与关联主 turn signal 的隔离取消控制器,防止云端超时耗尽本地恢复时间。同时严格检测终止错误与流中断,确保安全容灾且杜绝已取消 turn 的延迟文件提升。
|
package/lib/index.js
CHANGED
|
@@ -36,15 +36,18 @@ export function apply(ctx, config) {
|
|
|
36
36
|
if (ctx.logger) setLogger(ctx.logger)
|
|
37
37
|
refreshCatalogInBackground()
|
|
38
38
|
|
|
39
|
-
let
|
|
40
|
-
const
|
|
39
|
+
let localOverrides = null
|
|
40
|
+
const getSourceConfig = () => {
|
|
41
|
+
const liveSource = plainConfig(ctx.config ?? config ?? {})
|
|
42
|
+
return localOverrides ? { ...liveSource, ...localOverrides } : liveSource
|
|
43
|
+
}
|
|
41
44
|
const live = () => {
|
|
42
45
|
try {
|
|
43
|
-
const raw =
|
|
46
|
+
const raw = getSourceConfig() ?? {}
|
|
44
47
|
const validated = plainConfig(Config(plainConfig(raw)))
|
|
45
|
-
return plainConfig(validated) ??
|
|
48
|
+
return plainConfig(validated) ?? raw
|
|
46
49
|
} catch {
|
|
47
|
-
return
|
|
50
|
+
return getSourceConfig()
|
|
48
51
|
}
|
|
49
52
|
}
|
|
50
53
|
|
|
@@ -72,17 +75,13 @@ export function apply(ctx, config) {
|
|
|
72
75
|
|
|
73
76
|
const saveConfig = async (newFields) => {
|
|
74
77
|
const plain = plainConfig(newFields)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
} else if (settingsService && typeof settingsService.update === 'function') {
|
|
80
|
-
await settingsService.update(NS, plain)
|
|
81
|
-
}
|
|
82
|
-
} catch (err) {
|
|
83
|
-
logger.warn('[dsh-moa] Settings persistence warning:', err?.message || err)
|
|
78
|
+
if (settingsService && typeof settingsService.replace === 'function') {
|
|
79
|
+
await settingsService.replace(NS, plain)
|
|
80
|
+
} else if (settingsService && typeof settingsService.update === 'function') {
|
|
81
|
+
await settingsService.update(NS, plain)
|
|
84
82
|
}
|
|
85
|
-
|
|
83
|
+
localOverrides = { ...(localOverrides || {}), ...plain }
|
|
84
|
+
return live()
|
|
86
85
|
}
|
|
87
86
|
|
|
88
87
|
const callLlm = createLlmCaller(ctx)
|
|
@@ -110,6 +109,8 @@ export function apply(ctx, config) {
|
|
|
110
109
|
saveConfig,
|
|
111
110
|
Config,
|
|
112
111
|
plainConfig,
|
|
112
|
+
webServerPort: ctx.webServer?.port,
|
|
113
|
+
timeMachineEngine: ctx.get?.('timeMachineEngine') || ctx.timeMachineEngine,
|
|
113
114
|
})
|
|
114
115
|
|
|
115
116
|
// Session turn interceptor: route turn to real aggregator model & stream MoA synthesis
|
|
@@ -166,14 +167,15 @@ export function apply(ctx, config) {
|
|
|
166
167
|
if (userText.startsWith('/moa')) {
|
|
167
168
|
const cfg = live()
|
|
168
169
|
if (cfg.enabled !== false) {
|
|
169
|
-
const
|
|
170
|
-
const
|
|
170
|
+
const effectiveDefault = cfg.default_preset || 'default'
|
|
171
|
+
const parsed = parseMoACommand(userText, cfg.presets || [], effectiveDefault) || { prompt: userText, presetName: effectiveDefault, isExplicit: false }
|
|
172
|
+
const isExplicitPreset = Boolean(parsed.isExplicit || userText.includes('--preset') || userText.includes('-p '))
|
|
171
173
|
if (!isExplicitPreset && (cfg.smart_routing_enabled || cfg.presets?.some((p) => p.smart_routing_enabled))) {
|
|
172
174
|
try {
|
|
173
175
|
const routed = await resolvePresetForPrompt({
|
|
174
176
|
prompt: parsed.prompt || userText,
|
|
175
177
|
presets: cfg.presets || [],
|
|
176
|
-
defaultPreset: parsed.presetName ||
|
|
178
|
+
defaultPreset: parsed.presetName || effectiveDefault,
|
|
177
179
|
routingModel: cfg.smart_routing_model,
|
|
178
180
|
callLlm,
|
|
179
181
|
enabled: true,
|
|
@@ -185,7 +187,12 @@ export function apply(ctx, config) {
|
|
|
185
187
|
// fallback silently
|
|
186
188
|
}
|
|
187
189
|
}
|
|
188
|
-
const targetPreset = (cfg.presets || []).find((p) => p.name === parsed.presetName
|
|
190
|
+
const targetPreset = (cfg.presets || []).find((p) => p.name === (parsed.presetName || effectiveDefault))
|
|
191
|
+
|| (!isExplicitPreset ? (cfg.presets || []).find((p) => p.name === effectiveDefault) || cfg.presets?.[0] : null)
|
|
192
|
+
if (!targetPreset) {
|
|
193
|
+
logger.warn(`[dsh-moa] Unknown explicit preset "${parsed.presetName}" in /moa command.`)
|
|
194
|
+
return next()
|
|
195
|
+
}
|
|
189
196
|
const aggProvider = targetPreset?.aggregator?.provider || ''
|
|
190
197
|
const aggModel = targetPreset?.aggregator?.model || ''
|
|
191
198
|
const sessCwd = event.cwd || event.session?.cwd || (event.session?.path ? path.dirname(event.session.path) : undefined) || process.cwd()
|
package/lib/moa-budget.js
CHANGED
|
@@ -31,6 +31,7 @@ export function estimateCandidateRunCost(slots = [], prices = {}, promptTokens =
|
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Applies budget guardrails before initiating parallel candidate execution.
|
|
34
|
+
* Evaluates candidate runs, peer critique (Round 2) and downstream synthesis phases.
|
|
34
35
|
*/
|
|
35
36
|
export function applyBudgetGuardrails({
|
|
36
37
|
references = [],
|
|
@@ -39,6 +40,11 @@ export function applyBudgetGuardrails({
|
|
|
39
40
|
action = 'trim', // 'trim' | 'abort'
|
|
40
41
|
prices = {},
|
|
41
42
|
promptLength = 1000,
|
|
43
|
+
messages = [],
|
|
44
|
+
maxTokens = 2048,
|
|
45
|
+
peerCritiqueEnabled = false,
|
|
46
|
+
aggregator = null,
|
|
47
|
+
judgeModels = [],
|
|
42
48
|
}) {
|
|
43
49
|
if (!enabled || typeof maxBudgetUsd !== 'number' || maxBudgetUsd <= 0 || references.length === 0) {
|
|
44
50
|
return {
|
|
@@ -49,15 +55,52 @@ export function applyBudgetGuardrails({
|
|
|
49
55
|
}
|
|
50
56
|
}
|
|
51
57
|
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
// Calculate full input tokens from userPrompt length + messages context
|
|
59
|
+
let contextChars = Number(promptLength) || 1000
|
|
60
|
+
if (Array.isArray(messages) && messages.length > 0) {
|
|
61
|
+
for (const m of messages) {
|
|
62
|
+
if (typeof m?.content === 'string') {
|
|
63
|
+
contextChars += m.content.length
|
|
64
|
+
} else if (Array.isArray(m?.content)) {
|
|
65
|
+
for (const part of m.content) {
|
|
66
|
+
if (part?.text) contextChars += part.text.length
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const promptTokens = Math.max(50, Math.round(contextChars / 4))
|
|
72
|
+
const expectedOutputTokens = typeof maxTokens === 'number' && maxTokens > 0 ? maxTokens : 2048
|
|
73
|
+
|
|
74
|
+
// Base candidate cost per slot (Round 1)
|
|
75
|
+
const { totalEstimatedUsd, slotEstimates } = estimateCandidateRunCost(references, prices, promptTokens, expectedOutputTokens)
|
|
76
|
+
|
|
77
|
+
// Estimate fixed cost for downstream phases (Aggregator / Judge panel)
|
|
78
|
+
let downstreamUsd = 0
|
|
79
|
+
if (aggregator?.provider && aggregator?.model) {
|
|
80
|
+
const aggInput = promptTokens + expectedOutputTokens * references.length
|
|
81
|
+
const aggCost = estimateTokenCost(aggregator, { inputTokens: aggInput, outputTokens: expectedOutputTokens }, prices)
|
|
82
|
+
downstreamUsd += aggCost?.costUsd || 0
|
|
83
|
+
}
|
|
84
|
+
if (Array.isArray(judgeModels) && judgeModels.length > 0) {
|
|
85
|
+
for (const jm of judgeModels) {
|
|
86
|
+
if (jm?.provider && jm?.model) {
|
|
87
|
+
const jInput = promptTokens + expectedOutputTokens * references.length
|
|
88
|
+
const jCost = estimateTokenCost(jm, { inputTokens: jInput, outputTokens: expectedOutputTokens }, prices)
|
|
89
|
+
downstreamUsd += jCost?.costUsd || 0
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Multiplier if peer critique (Round 2) is enabled: each candidate runs another turn with input from all peers
|
|
95
|
+
const roundMultiplier = peerCritiqueEnabled ? 2 : 1
|
|
96
|
+
const fullEstimatedUsd = Number(((totalEstimatedUsd * roundMultiplier) + downstreamUsd).toFixed(6))
|
|
54
97
|
|
|
55
|
-
if (
|
|
98
|
+
if (fullEstimatedUsd <= maxBudgetUsd) {
|
|
56
99
|
return {
|
|
57
100
|
allowed: true,
|
|
58
101
|
references,
|
|
59
102
|
action: 'pass',
|
|
60
|
-
estimatedCostUsd:
|
|
103
|
+
estimatedCostUsd: fullEstimatedUsd,
|
|
61
104
|
}
|
|
62
105
|
}
|
|
63
106
|
|
|
@@ -66,9 +109,9 @@ export function applyBudgetGuardrails({
|
|
|
66
109
|
allowed: false,
|
|
67
110
|
references: [],
|
|
68
111
|
action: 'abort',
|
|
69
|
-
estimatedCostUsd:
|
|
112
|
+
estimatedCostUsd: fullEstimatedUsd,
|
|
70
113
|
maxBudgetUsd,
|
|
71
|
-
reason: `Estimated MoA run cost ($${
|
|
114
|
+
reason: `Estimated MoA run cost ($${fullEstimatedUsd.toFixed(4)}) exceeds configured max budget ($${maxBudgetUsd.toFixed(4)}).`,
|
|
72
115
|
}
|
|
73
116
|
}
|
|
74
117
|
|
|
@@ -78,9 +121,23 @@ export function applyBudgetGuardrails({
|
|
|
78
121
|
let cumulative = 0
|
|
79
122
|
|
|
80
123
|
for (const item of sorted) {
|
|
81
|
-
|
|
124
|
+
const candidateCost = item.costUsd * roundMultiplier
|
|
125
|
+
if (cumulative + candidateCost <= maxBudgetUsd) {
|
|
82
126
|
kept.push(item.slot)
|
|
83
|
-
cumulative +=
|
|
127
|
+
cumulative += candidateCost
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (kept.length === 0) {
|
|
132
|
+
return {
|
|
133
|
+
allowed: false,
|
|
134
|
+
references: [],
|
|
135
|
+
action: 'trim',
|
|
136
|
+
originalCount: references.length,
|
|
137
|
+
trimmedCount: 0,
|
|
138
|
+
estimatedCostUsd: Number((sorted[0]?.costUsd * roundMultiplier || 0).toFixed(6)),
|
|
139
|
+
maxBudgetUsd,
|
|
140
|
+
reason: `Cannot fit any candidate model within budget ($${maxBudgetUsd.toFixed(4)}). Cheapest candidate requires $${((sorted[0]?.costUsd || 0) * roundMultiplier).toFixed(4)}.`,
|
|
84
141
|
}
|
|
85
142
|
}
|
|
86
143
|
|
package/lib/moa-parser.js
CHANGED
|
@@ -40,7 +40,7 @@ export function parseRecommendedAssembler(judgeText, defaultIndex = 1, candidate
|
|
|
40
40
|
/**
|
|
41
41
|
* Parses a `/moa [preset] <prompt>` command invocation.
|
|
42
42
|
*/
|
|
43
|
-
export function parseMoACommand(text, presets = []) {
|
|
43
|
+
export function parseMoACommand(text, presets = [], defaultPreset = undefined) {
|
|
44
44
|
if (typeof text !== 'string' || !text.startsWith('/moa')) {
|
|
45
45
|
return null
|
|
46
46
|
}
|
|
@@ -59,8 +59,9 @@ export function parseMoACommand(text, presets = []) {
|
|
|
59
59
|
const remainder = text.slice(4).trim()
|
|
60
60
|
if (!remainder) {
|
|
61
61
|
return {
|
|
62
|
-
presetName:
|
|
62
|
+
presetName: defaultPreset,
|
|
63
63
|
prompt: '',
|
|
64
|
+
isExplicit: false,
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
|
|
@@ -69,6 +70,16 @@ export function parseMoACommand(text, presets = []) {
|
|
|
69
70
|
return {
|
|
70
71
|
presetName: matchPreset[1],
|
|
71
72
|
prompt: matchPreset[2] || '',
|
|
73
|
+
isExplicit: true,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const matchShortPreset = /^-p\s+([a-zA-Z0-9_-]+)\s*(.*)/s.exec(remainder)
|
|
78
|
+
if (matchShortPreset) {
|
|
79
|
+
return {
|
|
80
|
+
presetName: matchShortPreset[1],
|
|
81
|
+
prompt: matchShortPreset[2] || '',
|
|
82
|
+
isExplicit: true,
|
|
72
83
|
}
|
|
73
84
|
}
|
|
74
85
|
|
|
@@ -80,13 +91,15 @@ export function parseMoACommand(text, presets = []) {
|
|
|
80
91
|
return {
|
|
81
92
|
presetName: matched.name,
|
|
82
93
|
prompt: parts.slice(1).join(' ').trim(),
|
|
94
|
+
isExplicit: true,
|
|
83
95
|
}
|
|
84
96
|
}
|
|
85
97
|
}
|
|
86
98
|
|
|
87
99
|
return {
|
|
88
|
-
presetName:
|
|
100
|
+
presetName: defaultPreset,
|
|
89
101
|
prompt: remainder,
|
|
102
|
+
isExplicit: false,
|
|
90
103
|
}
|
|
91
104
|
}
|
|
92
105
|
|
package/lib/moa-runner.js
CHANGED
|
@@ -34,25 +34,11 @@ export async function runMoAPipeline({
|
|
|
34
34
|
liveCanvas = null,
|
|
35
35
|
onStreamDelta = null,
|
|
36
36
|
signal = null,
|
|
37
|
-
testGateEnabled = null,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
webServerPort = null,
|
|
41
|
-
timeMachineEngine = null,
|
|
42
|
-
timeMachineUrl = null,
|
|
43
|
-
force = isTestEnvironment() ? true : false,
|
|
44
|
-
checkpoint = true,
|
|
45
|
-
checkpointFn = null,
|
|
46
|
-
runId: customRunId = null,
|
|
37
|
+
testGateEnabled = null, testCommand = null, execFn = null,
|
|
38
|
+
webServerPort = null, timeMachineEngine = null, timeMachineUrl = null,
|
|
39
|
+
force = isTestEnvironment() ? true : false, checkpoint = true, checkpointFn = null, runId: customRunId = null,
|
|
47
40
|
}) {
|
|
48
|
-
const promoteOpts = {
|
|
49
|
-
webServerPort,
|
|
50
|
-
timeMachineEngine,
|
|
51
|
-
timeMachineUrl,
|
|
52
|
-
force: Boolean(force),
|
|
53
|
-
checkpoint,
|
|
54
|
-
checkpointFn,
|
|
55
|
-
}
|
|
41
|
+
const promoteOpts = { webServerPort, timeMachineEngine, timeMachineUrl, force: Boolean(force), checkpoint, checkpointFn }
|
|
56
42
|
if (signal?.aborted) return { error: new Error('Turn aborted') }
|
|
57
43
|
const startTime = Date.now()
|
|
58
44
|
// 1. Resolve configurations
|
|
@@ -81,6 +67,11 @@ export async function runMoAPipeline({
|
|
|
81
67
|
action: preset?.budget_action || 'trim',
|
|
82
68
|
prices,
|
|
83
69
|
promptLength: userPrompt?.length || 1000,
|
|
70
|
+
messages,
|
|
71
|
+
maxTokens,
|
|
72
|
+
peerCritiqueEnabled: isPeerCritiqueEnabled,
|
|
73
|
+
aggregator: primaryJudge,
|
|
74
|
+
judgeModels: preset?.multi_judge_enabled ? preset?.judge_models : [],
|
|
84
75
|
})
|
|
85
76
|
if (!budgetCheck.allowed) {
|
|
86
77
|
return {
|
|
@@ -358,7 +349,7 @@ export async function runMoAPipeline({
|
|
|
358
349
|
}))
|
|
359
350
|
const r2Prompt = buildPeerCritiquePrompt(userPrompt, cand.text, opponents, isBlindEvaluation)
|
|
360
351
|
try {
|
|
361
|
-
const slot =
|
|
352
|
+
const slot = cand.slot || { provider: cand.provider, model: cand.model }
|
|
362
353
|
const res = await callWithTransientRetry(callLlm, {
|
|
363
354
|
provider: slot.provider,
|
|
364
355
|
model: slot.model,
|
|
@@ -523,7 +514,7 @@ export async function runMoAPipeline({
|
|
|
523
514
|
const durationMs = Date.now() - startTime
|
|
524
515
|
const usageSummary = summarizeMoAUsage(referenceOutputs, aggUsage)
|
|
525
516
|
const winningRef = referenceOutputs[winningIndex - 1]
|
|
526
|
-
const winnerModel = winningRef?.label || slotLabel(referenceModels[0])
|
|
517
|
+
const winnerModel = winningRef?.label || slotLabel(effectiveReferences[0] || referenceModels[0])
|
|
527
518
|
const finalAggLabel = slotLabel(chosenJudge)
|
|
528
519
|
|
|
529
520
|
// Feature 9: Benchmark & Post-Mortem PR report
|
package/lib/routes.js
CHANGED
|
@@ -40,7 +40,7 @@ function readBody(req) {
|
|
|
40
40
|
})
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
export function registerMoaRoutes(ctx, { live, callLlm, liveCanvas, saveConfig, Config, plainConfig }) {
|
|
43
|
+
export function registerMoaRoutes(ctx, { live, callLlm, liveCanvas, saveConfig, Config, plainConfig, webServerPort, timeMachineEngine }) {
|
|
44
44
|
|
|
45
45
|
ctx.effect(() => {
|
|
46
46
|
return ctx.webServer.register({
|
|
@@ -220,7 +220,12 @@ ctx.effect(() => {
|
|
|
220
220
|
|
|
221
221
|
// Persist via saveConfig (in-memory and DSH profile settings)
|
|
222
222
|
if (typeof saveConfig === 'function') {
|
|
223
|
-
|
|
223
|
+
try {
|
|
224
|
+
await saveConfig(validated)
|
|
225
|
+
} catch (perr) {
|
|
226
|
+
writeJson(res, 500, { ok: false, error: 'Settings persistence failed: ' + (perr?.message || String(perr)) })
|
|
227
|
+
return
|
|
228
|
+
}
|
|
224
229
|
}
|
|
225
230
|
|
|
226
231
|
const unwrapped = typeof plainConfig === 'function' ? plainConfig(validated) : validated
|
|
@@ -563,13 +568,12 @@ ctx.effect(() => {
|
|
|
563
568
|
const preset = (cfg.presets || []).find((p) => p.name === presetName) || cfg.presets?.[0]
|
|
564
569
|
|
|
565
570
|
const result = await runMoAPipeline({
|
|
566
|
-
userPrompt: prompt,
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
liveCanvas,
|
|
571
|
+
userPrompt: prompt, messages: payload.messages || [], preset, callLlm,
|
|
572
|
+
cwd: payload.cwd || process.cwd(), prices: cfg.prices || {}, liveCanvas,
|
|
573
|
+
webServerPort: webServerPort || ctx.webServer?.port,
|
|
574
|
+
timeMachineEngine: timeMachineEngine || ctx.get?.('timeMachineEngine') || ctx.timeMachineEngine,
|
|
575
|
+
timeMachineUrl: payload.timeMachineUrl, force: payload.force,
|
|
576
|
+
checkpoint: payload.checkpoint, checkpointFn: payload.checkpointFn,
|
|
573
577
|
})
|
|
574
578
|
|
|
575
579
|
if (result?.skippedFiles > 0) {
|