@ddtcorex/dsh-maestro-supervisor 0.6.6 → 0.6.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/debug-agent.js +78 -22
- package/lib/health-poller.js +28 -2
- package/lib/supervisor.js +1 -1
- package/package.json +1 -1
package/lib/debug-agent.js
CHANGED
|
@@ -217,20 +217,40 @@ function defaultWriteFile(p, c) {
|
|
|
217
217
|
writeFileSync(p, c, 'utf-8');
|
|
218
218
|
}
|
|
219
219
|
async function defaultDryBoot() {
|
|
220
|
+
const { execSync } = await import('node:child_process');
|
|
221
|
+
const { resolveDeepseekHarnessDir } = await import('./paths.js');
|
|
222
|
+
const { buildKillStalePortsCommand } = await import('./restart-guards.js');
|
|
223
|
+
const port = Math.floor(19000 + Math.random() * 1000);
|
|
224
|
+
let tmp = '';
|
|
220
225
|
try {
|
|
221
|
-
const { execSync } = await import('node:child_process');
|
|
222
|
-
const { resolveDeepseekHarnessDir } = await import('./paths.js');
|
|
223
226
|
const deepseekDir = resolveDeepseekHarnessDir();
|
|
224
|
-
|
|
225
|
-
const port = Math.floor(19000 + Math.random() * 1000);
|
|
227
|
+
tmp = execSync('mktemp -d', { encoding: 'utf-8' }).trim();
|
|
226
228
|
// Use spawn-based dry-boot to avoid nested quoting hell (prev \\$! / \\$(seq) caused syntax error)
|
|
227
|
-
const out = execSync(`timeout 8 bash -c 'DSH_HOME=${tmp} pnpm --dir ${deepseekDir} dsh web --port ${port} --no-open >${tmp}/dsh.log 2>&1 & pid=$!; for i in $(seq 1 5); do sleep 1; if curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:${port}/ 2>&1 | grep -q 200; then
|
|
228
|
-
execSync(`rm -rf ${tmp}`);
|
|
229
|
+
const out = execSync(`timeout 8 bash -c 'DSH_HOME=${tmp} pnpm --dir ${deepseekDir} dsh web --port ${port} --no-open >${tmp}/dsh.log 2>&1 & pid=$!; for i in $(seq 1 5); do sleep 1; if curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:${port}/ 2>&1 | grep -q 200; then echo ok; exit 0; fi; done; echo fail; exit 1'`, { encoding: 'utf-8', timeout: 12000 });
|
|
229
230
|
return out.includes('ok');
|
|
230
231
|
}
|
|
231
232
|
catch {
|
|
232
233
|
return false;
|
|
233
234
|
}
|
|
235
|
+
finally {
|
|
236
|
+
// `pnpm dsh web` is a pnpm→sh→node tree (see AGENTS.md Known Issues); the
|
|
237
|
+
// backgrounded job's `$!` above is only the pnpm wrapper, so `kill $pid`
|
|
238
|
+
// never reached the real node process — it kept running detached,
|
|
239
|
+
// holding the ephemeral port. Confirmed live 2026-08-31: 3 failed
|
|
240
|
+
// attempts left 3 orphaned "MainThread" node processes eating 6.4G RAM
|
|
241
|
+
// for 7+ hours. Resolve the real pid by the port it is actually
|
|
242
|
+
// listening on instead (same technique as the live-restart kill step).
|
|
243
|
+
try {
|
|
244
|
+
execSync(buildKillStalePortsCommand([port]), { timeout: 5000, stdio: 'pipe' });
|
|
245
|
+
}
|
|
246
|
+
catch { }
|
|
247
|
+
if (tmp) {
|
|
248
|
+
try {
|
|
249
|
+
execSync(`rm -rf ${tmp}`);
|
|
250
|
+
}
|
|
251
|
+
catch { }
|
|
252
|
+
}
|
|
253
|
+
}
|
|
234
254
|
}
|
|
235
255
|
async function defaultFetchLLM(prompt) {
|
|
236
256
|
const cfg = await resolveLLMConfig();
|
|
@@ -248,6 +268,7 @@ async function defaultFetchLLM(prompt) {
|
|
|
248
268
|
body: JSON.stringify({
|
|
249
269
|
model: cfg.model,
|
|
250
270
|
temperature: 0.2,
|
|
271
|
+
...(cfg.reasoningEffort ? { reasoning_effort: cfg.reasoningEffort, reasoningEffort: cfg.reasoningEffort } : {}),
|
|
251
272
|
messages: [
|
|
252
273
|
{ role: 'system', content: 'You are a systematic-debugging agent for DSH Web resilience. Follow the 4 phases: root cause, pattern analysis, hypothesis, implementation. Always propose minimal single-file fix.' },
|
|
253
274
|
{ role: 'user', content: prompt },
|
|
@@ -292,7 +313,7 @@ async function defaultFetchLLM(prompt) {
|
|
|
292
313
|
{ role: 'user', content: prompt },
|
|
293
314
|
],
|
|
294
315
|
max_output_tokens: 4000,
|
|
295
|
-
reasoning: { effort: 'low' },
|
|
316
|
+
...(cfg.reasoningEffort ? { reasoning: { effort: cfg.reasoningEffort } } : { reasoning: { effort: 'low' } }),
|
|
296
317
|
}),
|
|
297
318
|
});
|
|
298
319
|
if (!res2.ok)
|
|
@@ -319,7 +340,9 @@ async function resolveLLMConfig() {
|
|
|
319
340
|
// Model: AI_MODEL / DEEPSEEK_MODEL / settings.json domains.supervisor.model -> domains.review.model / default
|
|
320
341
|
// Supervisor has its own picker; falls back to review model for backward compat, then DSH default.
|
|
321
342
|
let model = process.env.AI_MODEL ?? process.env.DEEPSEEK_MODEL ?? process.env.OPENAI_MODEL ?? null;
|
|
322
|
-
|
|
343
|
+
// reasoningEffort: env first, then supervisor -> review, then settings.yaml fallback
|
|
344
|
+
let reasoningEffort = (process.env.AI_REASONING_EFFORT ?? process.env.REASONING_EFFORT ?? null) ?? undefined;
|
|
345
|
+
if (!model || !reasoningEffort) {
|
|
323
346
|
try {
|
|
324
347
|
const { readFileSync } = await import('node:fs');
|
|
325
348
|
const { homedir } = await import('node:os');
|
|
@@ -327,24 +350,57 @@ async function resolveLLMConfig() {
|
|
|
327
350
|
const raw = readFileSync(settingsPath, 'utf-8');
|
|
328
351
|
const j = JSON.parse(raw);
|
|
329
352
|
// Prefer supervisor model, fall back to review model (so old installs keep working)
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
353
|
+
if (!model) {
|
|
354
|
+
const sup = j?.domains?.supervisor?.model?.model ?? j?.domains?.supervisor?.model;
|
|
355
|
+
const rev = j?.domains?.review?.model?.model ?? j?.domains?.review?.model;
|
|
356
|
+
let m = null;
|
|
357
|
+
if (typeof sup === 'string')
|
|
358
|
+
m = sup;
|
|
359
|
+
else if (sup?.model && typeof sup.model === 'string')
|
|
360
|
+
m = sup.model;
|
|
361
|
+
else if (typeof rev === 'string')
|
|
362
|
+
m = rev;
|
|
363
|
+
else if (rev?.model && typeof rev.model === 'string')
|
|
364
|
+
m = rev.model;
|
|
365
|
+
if (typeof m === 'string' && m.trim() !== '')
|
|
366
|
+
model = m;
|
|
367
|
+
}
|
|
368
|
+
if (!reasoningEffort) {
|
|
369
|
+
// config-lib validator: model is {provider, model, reasoningEffort?} inside domains.supervisor.model / domains.review.model
|
|
370
|
+
// Support both nested object and flat legacy fallback for future compat
|
|
371
|
+
const supObj = j?.domains?.supervisor?.model;
|
|
372
|
+
const revObj = j?.domains?.review?.model;
|
|
373
|
+
const supEff = typeof supObj === 'object' && supObj !== null ? supObj.reasoningEffort : undefined;
|
|
374
|
+
const supFlat = j?.domains?.supervisor?.reasoningEffort;
|
|
375
|
+
const revEff = typeof revObj === 'object' && revObj !== null ? revObj.reasoningEffort : undefined;
|
|
376
|
+
const revFlat = j?.domains?.review?.reasoningEffort;
|
|
377
|
+
const candidate = supEff ?? supFlat ?? revEff ?? revFlat;
|
|
378
|
+
if (typeof candidate === 'string' && candidate.trim() !== '')
|
|
379
|
+
reasoningEffort = candidate.trim();
|
|
380
|
+
}
|
|
343
381
|
}
|
|
344
382
|
catch { }
|
|
345
383
|
}
|
|
346
384
|
if (!model)
|
|
347
385
|
model = 'deepseek-chat';
|
|
386
|
+
// Also try settings.yaml for reasoningEffort if still missing (e.g., llm-pi-ai default reasoning)
|
|
387
|
+
if (!reasoningEffort) {
|
|
388
|
+
try {
|
|
389
|
+
const { readFileSync } = await import('node:fs');
|
|
390
|
+
const { homedir } = await import('node:os');
|
|
391
|
+
const yamlPath = `${homedir()}/.dsh/settings.yaml`;
|
|
392
|
+
const yaml = readFileSync(yamlPath, 'utf-8');
|
|
393
|
+
// Look for reasoningEffort near the model id in provider blocks (future-proof; currently not in yaml)
|
|
394
|
+
const re = new RegExp(`id:\\s*${model}[\\s\\S]{0,200}reasoningEffort:\\s*(\\S+)`, 'm');
|
|
395
|
+
const m = yaml.match(re);
|
|
396
|
+
if (m) {
|
|
397
|
+
const v = m[1].trim().replace(/^["']|["']$/g, '');
|
|
398
|
+
if (v)
|
|
399
|
+
reasoningEffort = v;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
catch { }
|
|
403
|
+
}
|
|
348
404
|
// If url not set via env, try to resolve from ~/.dsh/settings.yaml llm-pi-ai providers (DeepSeek suggested setup)
|
|
349
405
|
if (!url) {
|
|
350
406
|
try {
|
|
@@ -412,7 +468,7 @@ async function resolveLLMConfig() {
|
|
|
412
468
|
if (!url.includes('/v1/') && !url.includes('/chat/completions')) {
|
|
413
469
|
finalUrl = url.replace(/\/$/, '') + '/v1/chat/completions';
|
|
414
470
|
}
|
|
415
|
-
return { key, url: finalUrl, model };
|
|
471
|
+
return { key, url: finalUrl, model, ...(reasoningEffort ? { reasoningEffort } : {}) };
|
|
416
472
|
}
|
|
417
473
|
async function resolveApiKey() {
|
|
418
474
|
const cfg = await resolveLLMConfig();
|
package/lib/health-poller.js
CHANGED
|
@@ -50,6 +50,10 @@ function isRecentlyStarted(opts, wallMs) {
|
|
|
50
50
|
return true;
|
|
51
51
|
return false;
|
|
52
52
|
}
|
|
53
|
+
// Specific parse/boot-failure markers only. Bare 'JSON'/'YAML' were removed
|
|
54
|
+
// (2026-08-31): they matched any line whose payload merely *contained* those
|
|
55
|
+
// substrings — e.g. maestro-sync's status JSON listing session.jsonl.zstd /
|
|
56
|
+
// settings.json paths — turning a healthy 401 into a rollback + restart.
|
|
53
57
|
const ERROR_PATTERNS = [
|
|
54
58
|
'ERR_MODULE_NOT_FOUND',
|
|
55
59
|
'ERR_PNPM',
|
|
@@ -58,8 +62,6 @@ const ERROR_PATTERNS = [
|
|
|
58
62
|
'SyntaxError',
|
|
59
63
|
'YAMLParseError',
|
|
60
64
|
'ParseError',
|
|
61
|
-
'YAML',
|
|
62
|
-
'JSON',
|
|
63
65
|
'corrupted',
|
|
64
66
|
'allowBuilds',
|
|
65
67
|
'Cannot find module',
|
|
@@ -198,6 +200,30 @@ export async function pollHealth(opts = {}) {
|
|
|
198
200
|
logTail: logContent.slice(-5000),
|
|
199
201
|
};
|
|
200
202
|
}
|
|
203
|
+
// Corroborate with a cheap port-liveness check before declaring a crash.
|
|
204
|
+
// The HTTP fetch shares dsh-web's own event loop, so a busy-but-alive
|
|
205
|
+
// process (e.g. heavy GitLab-webhook-triggered review work) times out
|
|
206
|
+
// the same way a genuinely dead one does — but a real crash always frees
|
|
207
|
+
// the port, while `ss` does not depend on the contended event loop to
|
|
208
|
+
// answer. Downgrade to DEGRADED (still visible, still escalates after
|
|
209
|
+
// repeated ticks) instead of forcing an immediate rollback + restartWeb
|
|
210
|
+
// that would kill an in-flight review for nothing.
|
|
211
|
+
let portAlive = false;
|
|
212
|
+
try {
|
|
213
|
+
portAlive = await psAliveFn();
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
portAlive = false;
|
|
217
|
+
}
|
|
218
|
+
if (portAlive) {
|
|
219
|
+
return {
|
|
220
|
+
up: true,
|
|
221
|
+
httpCode,
|
|
222
|
+
error: logError ? `${fetchError} + ${logError}` : fetchError,
|
|
223
|
+
degraded: true,
|
|
224
|
+
logTail: logContent.slice(-5000),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
201
227
|
return {
|
|
202
228
|
up: false,
|
|
203
229
|
httpCode,
|
package/lib/supervisor.js
CHANGED
package/package.json
CHANGED