@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 +4 -4
- package/lib/cli.js +6 -0
- package/lib/client.js +14 -10
- package/lib/health-poller.js +41 -6
- package/lib/types/client/auto-reload.d.ts.map +1 -1
- package/package.json +1 -1
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
|
|
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":["--
|
|
117
|
-
# → {"type":"server-response","rpcId":"r2","result":{"ok":true,"value":{"resumed":["--
|
|
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:
|
|
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
|
-
|
|
51
|
+
if (wasPolling)
|
|
52
|
+
void checkAndReload();
|
|
51
53
|
};
|
|
52
|
-
// DSH Web uses WebSocket for sessions; a close
|
|
53
|
-
//
|
|
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
|
|
65
|
-
|
|
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
|
package/lib/health-poller.js
CHANGED
|
@@ -39,14 +39,36 @@ export async function pollHealth(opts = {}) {
|
|
|
39
39
|
// ignore log read errors
|
|
40
40
|
}
|
|
41
41
|
let logError;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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,
|
|
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