@ddtcorex/dsh-maestro-supervisor 0.6.0 → 0.6.2

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 CHANGED
@@ -31,7 +31,7 @@ The package declares `dsh.client` (`platform: web`, `inject: ["@deepseek-ai/dsh-
31
31
  dsh plugin --profile web add @ddtcorex/dsh-maestro-supervisor
32
32
  # or manually:
33
33
  # edit ~/.dsh/profiles/web/package.json:
34
- # "@ddtcorex/dsh-maestro-supervisor": "link:/home/kai/Work/htdocs/maestro-harness/packages/dsh-maestro-supervisor"
34
+ # "@ddtcorex/dsh-maestro-supervisor": "link:<workspace-root>/packages/dsh-maestro-supervisor"
35
35
  pnpm --dir ~/.dsh/profiles/web install
36
36
  ls -l ~/.dsh/profiles/web/node_modules/@ddtcorex/dsh-maestro-supervisor # → .../packages/dsh-maestro-supervisor
37
37
  ```
@@ -113,8 +113,8 @@ curl -s http://127.0.0.1:3080/dsh-maestro-supervisor-resume/scan -X POST \
113
113
  # Resume (re-attaches agent + followup continue, recovers provider/model)
114
114
  curl -s http://127.0.0.1:3080/dsh-maestro-supervisor-resume/resume -X POST \
115
115
  -H 'content-type: application/json' \
116
- -d '{"type":"client-request","rpcId":"r2","method":"resume","payload":{"ids":["--home-kai-Work-htdocs-maestro-harness--/session-abc"]}}'
117
- # → {"type":"server-response","rpcId":"r2","result":{"ok":true,"value":{"resumed":["--home-kai-Work-htdocs-maestro-harness--/session-abc"]}}}
116
+ -d '{"type":"client-request","rpcId":"r2","method":"resume","payload":{"ids":["--example-project--/session-abc"]}}'
117
+ # → {"type":"server-response","rpcId":"r2","result":{"ok":true,"value":{"resumed":["--example-project--/session-abc"]}}}
118
118
  # or {"ok":false,"error":{"code":"bad-request","message":"resume requires at least one session id"}}
119
119
  ```
120
120
 
@@ -191,6 +191,6 @@ Hard mode (optional): `package.json` add `"@ddtcorex/dsh-maestro-notifier": "wor
191
191
 
192
192
  ## See Also
193
193
 
194
- - Spec: `docs/specs/2026-08-27-dsh-web-resilience-design.md`
194
+ - Spec: `<workspace-root>/docs/specs/2026-08-27-dsh-web-resilience-design.md`
195
195
  - Skill: `maestro-skills/skills/dsh-safe-web-update/` (`restart-dsh-web.sh` with `dry_boot_and_verify()` and `--auto`)
196
196
  - Client bundling: `dsh-maestro-mobile` (`scripts/build-client.mjs` pattern)
package/lib/cli.js CHANGED
@@ -122,6 +122,12 @@ Commands:
122
122
  },
123
123
  restartWeb: async () => {
124
124
  const { execSync } = await import('node:child_process');
125
+ // Kill stale MainThread holding 3080/3000 before any restart attempt
126
+ // (EADDRINUSE crash leaves old pid alive with http 200; new start would fail)
127
+ try {
128
+ execSync(`pids=$(ss -tlnp 2>/dev/null | sed -n 's/.*pid=\\([0-9]*\\).*/\\1/p' | sort -u); if [ -n "$pids" ]; then echo "[supervisor] killing stale pids $pids"; kill $pids 2>/dev/null || true; sleep 2; fi`, { timeout: 5000, stdio: 'pipe' });
129
+ }
130
+ catch { }
125
131
  // Prefer systemd — if dsh-web.service is installed, restart/start it
126
132
  try {
127
133
  execSync('systemctl --user is-active --quiet dsh-web.service && systemctl --user restart dsh-web.service || systemctl --user start dsh-web.service', { timeout: 15000, stdio: 'pipe' });
package/lib/client.js CHANGED
@@ -46,11 +46,13 @@ function apply(ctx) {
46
46
  };
47
47
  const onOffline = () => startPolling();
48
48
  const onOnline = () => {
49
+ const wasPolling = !!timer;
49
50
  stopPolling();
50
- void checkAndReload();
51
+ if (wasPolling)
52
+ void checkAndReload();
51
53
  };
52
- // DSH Web uses WebSocket for sessions; a close means the server went down.
53
- // We hook the global WebSocket to detect closes without polling constantly.
54
+ // DSH Web uses WebSocket for sessions; a close does NOT necessarily mean server down
55
+ // (normal session end also closes WS). Only start polling if HEAD fetch actually fails.
54
56
  const OriginalWebSocket = window.WebSocket;
55
57
  let wsCloseHandler = null;
56
58
  try {
@@ -58,15 +60,17 @@ function apply(ctx) {
58
60
  const Patched = function (url, protocols) {
59
61
  const ws = protocols ? new OriginalWebSocket(url, protocols) : new OriginalWebSocket(url);
60
62
  ws.addEventListener('close', () => {
61
- // Only treat DSH WebSocket closes (same origin) as server-down
62
63
  try {
63
64
  const u = new URL(url, window.location.href);
64
- if (u.host === window.location.host)
65
- startPolling();
66
- }
67
- catch {
68
- startPolling();
65
+ if (u.host !== window.location.host)
66
+ return;
69
67
  }
68
+ catch { /* ignore */ }
69
+ // Verify server is actually down before polling — avoid reload on normal WS close
70
+ fetch('/', { method: 'HEAD', cache: 'no-store' }).then(res => {
71
+ if (!res.ok)
72
+ startPolling();
73
+ }).catch(() => startPolling());
70
74
  });
71
75
  ws.addEventListener('open', () => {
72
76
  // If we were polling and WS reopens, check immediately
@@ -85,7 +89,7 @@ function apply(ctx) {
85
89
  window.addEventListener('offline', onOffline);
86
90
  window.addEventListener('online', onOnline);
87
91
  document.addEventListener('visibilitychange', () => {
88
- if (document.visibilityState === 'visible')
92
+ if (document.visibilityState === 'visible' && timer)
89
93
  void checkAndReload();
90
94
  });
91
95
  // Host push: POST /dsh-maestro-supervisor-reload/reload (loopback) -> reload
@@ -39,14 +39,36 @@ export async function pollHealth(opts = {}) {
39
39
  // ignore log read errors
40
40
  }
41
41
  let logError;
42
- const lowerLog = logContent.toLowerCase();
43
- for (const pat of ERROR_PATTERNS) {
44
- if (lowerLog.includes(pat.toLowerCase())) {
45
- // extract line containing pattern (case-insensitive)
46
- const line = logContent.split('\n').find(l => l.toLowerCase().includes(pat.toLowerCase())) ?? pat;
47
- logError = line.trim().slice(0, 500);
42
+ // Find most recent error line (not first) and ignore stale errors that are
43
+ // followed by a successful boot (log tail is append-only, old EADDRINUSE stays forever).
44
+ // We check last occurrence and ensure no "dsh web: http" success after it.
45
+ const lines = logContent.split('\n');
46
+ const lowerLines = lines.map(l => l.toLowerCase());
47
+ let lastErrorIdx = -1;
48
+ let matchedLine = '';
49
+ for (let i = lines.length - 1; i >= 0; i--) {
50
+ const lower = lowerLines[i];
51
+ for (const pat of ERROR_PATTERNS) {
52
+ if (lower.includes(pat.toLowerCase())) {
53
+ lastErrorIdx = i;
54
+ matchedLine = lines[i].trim().slice(0, 500);
55
+ break;
56
+ }
57
+ }
58
+ if (lastErrorIdx !== -1)
48
59
  break;
60
+ }
61
+ if (lastErrorIdx !== -1) {
62
+ // If a successful boot line appears after the last error, error is stale (already recovered)
63
+ let hasSuccessAfter = false;
64
+ for (let i = lastErrorIdx + 1; i < lines.length; i++) {
65
+ if (lowerLines[i].includes('dsh web: http')) {
66
+ hasSuccessAfter = true;
67
+ break;
68
+ }
49
69
  }
70
+ if (!hasSuccessAfter)
71
+ logError = matchedLine;
50
72
  }
51
73
  // Distinguish FULL (http !=200) vs DEGRADED (http 200 but log has plugin error)
52
74
  if (fetchError) {
@@ -59,6 +81,19 @@ export async function pollHealth(opts = {}) {
59
81
  };
60
82
  }
61
83
  if (logError) {
84
+ // EADDRINUSE is fatal even with http 200 — old process still holds 3080/3000
85
+ // and new start failed; treat as FULL down so supervisor kills + restarts.
86
+ const lowerErr = logError.toLowerCase();
87
+ const isFatalPortError = lowerErr.includes('eaddrinuse') || lowerErr.includes('address already in use');
88
+ if (isFatalPortError) {
89
+ return {
90
+ up: false,
91
+ httpCode,
92
+ error: logError,
93
+ degraded: false,
94
+ logTail: logContent.slice(-5000),
95
+ };
96
+ }
62
97
  // http 200 but log error → DEGRADED (isolatable), not FULL
63
98
  if (httpCode === 200) {
64
99
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"auto-reload.d.ts","sourceRoot":"","sources":["../../../src/client/auto-reload.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,wBAAgB,KAAK,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI,CA4FpC"}
1
+ {"version":3,"file":"auto-reload.d.ts","sourceRoot":"","sources":["../../../src/client/auto-reload.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,wBAAgB,KAAK,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI,CAgGpC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ddtcorex/dsh-maestro-supervisor",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
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",