@ddtcorex/dsh-maestro-supervisor 0.6.0 → 0.6.1
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/health-poller.js +41 -6
- 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/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 {
|
package/package.json
CHANGED