@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.
@@ -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
- const tmp = execSync('mktemp -d', { encoding: 'utf-8' }).trim();
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 kill $pid 2>/dev/null || true; wait $pid 2>/dev/null || true; echo ok; exit 0; fi; done; kill $pid 2>/dev/null || true; wait $pid 2>/dev/null || true; echo fail; exit 1'`, { encoding: 'utf-8', timeout: 12000 });
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
- if (!model) {
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
- const sup = j?.domains?.supervisor?.model?.model ?? j?.domains?.supervisor?.model;
331
- const rev = j?.domains?.review?.model?.model ?? j?.domains?.review?.model;
332
- let m = null;
333
- if (typeof sup === 'string')
334
- m = sup;
335
- else if (sup?.model && typeof sup.model === 'string')
336
- m = sup.model;
337
- else if (typeof rev === 'string')
338
- m = rev;
339
- else if (rev?.model && typeof rev.model === 'string')
340
- m = rev.model;
341
- if (typeof m === 'string' && m.trim() !== '')
342
- model = m;
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();
@@ -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
@@ -197,7 +197,7 @@ export class Supervisor {
197
197
  return cfg.downThreshold;
198
198
  }
199
199
  catch { }
200
- return 3;
200
+ return 5;
201
201
  }
202
202
  async findInterruptedRecent(withinMs) {
203
203
  const ms = withinMs ?? this.getResumeWithinMs();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ddtcorex/dsh-maestro-supervisor",
3
- "version": "0.6.6",
3
+ "version": "0.6.8",
4
4
  "description": "Supervisor daemon for DSH Web resilience — auto-detect crashes, rollback to LKG, report",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",