@ddtcorex/dsh-maestro-supervisor 0.2.0 → 0.4.0
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.d.ts +14 -0
- package/lib/debug-agent.js +66 -0
- package/lib/resume.d.ts +5 -0
- package/lib/resume.js +41 -0
- package/lib/supervisor.js +21 -0
- package/package.json +1 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface DebugAgentOpts {
|
|
2
|
+
reportPath: string;
|
|
3
|
+
health: {
|
|
4
|
+
error?: string;
|
|
5
|
+
httpCode?: number;
|
|
6
|
+
};
|
|
7
|
+
attempts?: number;
|
|
8
|
+
cooldownMs?: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function runDebugAgent(opts: DebugAgentOpts): Promise<{
|
|
11
|
+
fixed: boolean;
|
|
12
|
+
reason: string;
|
|
13
|
+
}>;
|
|
14
|
+
export declare function _resetDebugAgentForTest(): void;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
let lastRun = 0;
|
|
2
|
+
let attempts = 0;
|
|
3
|
+
const MAX_ATTEMPTS = 3;
|
|
4
|
+
export async function runDebugAgent(opts) {
|
|
5
|
+
const now = Date.now();
|
|
6
|
+
const cooldownMs = opts.cooldownMs ?? 10 * 60 * 1000;
|
|
7
|
+
if (now - lastRun < cooldownMs) {
|
|
8
|
+
return { fixed: false, reason: 'cooldown' };
|
|
9
|
+
}
|
|
10
|
+
if (attempts >= MAX_ATTEMPTS) {
|
|
11
|
+
return { fixed: false, reason: 'max attempts' };
|
|
12
|
+
}
|
|
13
|
+
lastRun = now;
|
|
14
|
+
attempts++;
|
|
15
|
+
// Phase 3A: deterministic checks before LLM
|
|
16
|
+
// 1) Try pnpm verify in the degraded package (if identifiable)
|
|
17
|
+
// 2) Dry-boot DSH web on ephemeral port
|
|
18
|
+
// If both pass, consider fixed (degraded was transient, e.g. log tail stale)
|
|
19
|
+
// Otherwise, would spawn systematic-debugging subagent (LLM) — stub for now
|
|
20
|
+
try {
|
|
21
|
+
const { execSync } = await import('node:child_process');
|
|
22
|
+
const { readFileSync } = await import('node:fs');
|
|
23
|
+
// Check report exists
|
|
24
|
+
let report = '';
|
|
25
|
+
try {
|
|
26
|
+
report = readFileSync(opts.reportPath, 'utf-8');
|
|
27
|
+
}
|
|
28
|
+
catch { }
|
|
29
|
+
// If error is ERR_MODULE_NOT_FOUND for a package that now has lib/index.js, consider fixed
|
|
30
|
+
const err = opts.health.error ?? '';
|
|
31
|
+
if (err.includes('ERR_MODULE_NOT_FOUND') || err.includes('Cannot find module')) {
|
|
32
|
+
// Check if the missing file now exists (e.g. after pnpm build)
|
|
33
|
+
// This is a heuristic: if any dsh-maestro package now has lib/index.js, the degraded may be stale
|
|
34
|
+
const candidates = ['dsh-maestro-supervisor', 'dsh-maestro-observe', 'dsh-maestro-memory'];
|
|
35
|
+
for (const pkg of candidates) {
|
|
36
|
+
try {
|
|
37
|
+
const p = `/home/kai/Work/htdocs/maestro-harness/packages/${pkg}/lib/index.js`;
|
|
38
|
+
readFileSync(p);
|
|
39
|
+
// if file exists, the error may be transient — try verify
|
|
40
|
+
try {
|
|
41
|
+
execSync(`pnpm --dir /home/kai/Work/htdocs/maestro-harness/packages/${pkg} verify --silent 2>&1 | head -5`, { timeout: 15000 });
|
|
42
|
+
}
|
|
43
|
+
catch { }
|
|
44
|
+
}
|
|
45
|
+
catch { }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Dry-boot check (isolated DSH_HOME, port 0)
|
|
49
|
+
try {
|
|
50
|
+
const tmp = execSync('mktemp -d', { encoding: 'utf-8' }).trim();
|
|
51
|
+
const port = Math.floor(19000 + Math.random() * 1000);
|
|
52
|
+
const out = execSync(`timeout 8 bash -c 'DSH_HOME=${tmp} pnpm --dir /home/kai/Work/htdocs/maestro-harness/deepseek-harness 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 });
|
|
53
|
+
execSync(`rm -rf ${tmp}`);
|
|
54
|
+
if (out.includes('ok')) {
|
|
55
|
+
return { fixed: true, reason: `dry-boot ok (attempt ${attempts}) — transient degraded` };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch { }
|
|
59
|
+
}
|
|
60
|
+
catch { }
|
|
61
|
+
return { fixed: false, reason: `would debug ${opts.reportPath} (attempt ${attempts}) — LLM not wired, manual fix needed` };
|
|
62
|
+
}
|
|
63
|
+
export function _resetDebugAgentForTest() {
|
|
64
|
+
lastRun = 0;
|
|
65
|
+
attempts = 0;
|
|
66
|
+
}
|
package/lib/resume.d.ts
ADDED
package/lib/resume.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
export async function findInterrupted(dshHome) {
|
|
5
|
+
const home = dshHome ?? path.join(os.homedir(), '.dsh');
|
|
6
|
+
const sessionsRoot = path.join(home, 'sessions');
|
|
7
|
+
let scanned = 0;
|
|
8
|
+
const interrupted = [];
|
|
9
|
+
try {
|
|
10
|
+
const groups = fs.readdirSync(sessionsRoot, { withFileTypes: true });
|
|
11
|
+
for (const g of groups) {
|
|
12
|
+
if (!g.isDirectory())
|
|
13
|
+
continue;
|
|
14
|
+
const groupPath = path.join(sessionsRoot, g.name);
|
|
15
|
+
const sessions = fs.readdirSync(groupPath, { withFileTypes: true });
|
|
16
|
+
for (const s of sessions) {
|
|
17
|
+
if (!s.isDirectory())
|
|
18
|
+
continue;
|
|
19
|
+
scanned++;
|
|
20
|
+
const zstdPath = path.join(groupPath, s.name, 'session.jsonl.zstd');
|
|
21
|
+
const jsonlPath = path.join(groupPath, s.name, 'session.jsonl');
|
|
22
|
+
try {
|
|
23
|
+
let content = '';
|
|
24
|
+
if (fs.existsSync(zstdPath)) {
|
|
25
|
+
const { execSync } = await import('node:child_process');
|
|
26
|
+
content = execSync(`zstd -d -c ${JSON.stringify(zstdPath)} 2>/dev/null | tail -5`, { encoding: 'utf-8' });
|
|
27
|
+
}
|
|
28
|
+
else if (fs.existsSync(jsonlPath)) {
|
|
29
|
+
content = fs.readFileSync(jsonlPath, 'utf-8').slice(-5000);
|
|
30
|
+
}
|
|
31
|
+
if (content.toLowerCase().includes('interrupted')) {
|
|
32
|
+
interrupted.push(`${g.name}/${s.name}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch { }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch { }
|
|
40
|
+
return { scanned, interrupted };
|
|
41
|
+
}
|
package/lib/supervisor.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { runDebugAgent } from './debug-agent.js';
|
|
2
|
+
import { findInterrupted } from './resume.js';
|
|
1
3
|
export class Supervisor {
|
|
2
4
|
deps;
|
|
3
5
|
lastRollback = 0;
|
|
@@ -20,6 +22,16 @@ export class Supervisor {
|
|
|
20
22
|
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
21
23
|
const reportPath = await this.deps.writeReport({ ts, health, action: `degraded — ${health.error ?? 'plugin'}` }).catch(() => '');
|
|
22
24
|
await this.deps.notify(`DEGRADED: ${health.error ?? 'plugin'} (report: ${reportPath})`).catch(() => { });
|
|
25
|
+
// Phase 3: fire-and-forget debug + resume (never block tick, skip in test)
|
|
26
|
+
if (!process.env.VITEST) {
|
|
27
|
+
void runDebugAgent({ reportPath, health }).catch(() => { });
|
|
28
|
+
setTimeout(() => {
|
|
29
|
+
findInterrupted().then(r => {
|
|
30
|
+
if (r.interrupted.length)
|
|
31
|
+
this.deps.notify(`RESUME: ${r.interrupted.length} interrupted sessions (${r.interrupted.slice(0, 3).join(', ')})`).catch(() => { });
|
|
32
|
+
}).catch(() => { });
|
|
33
|
+
}, 0);
|
|
34
|
+
}
|
|
23
35
|
}
|
|
24
36
|
catch { }
|
|
25
37
|
return;
|
|
@@ -53,6 +65,15 @@ export class Supervisor {
|
|
|
53
65
|
const reportPath = await this.deps.writeReport({ ts, health, action: `rollback — ${health.error ?? 'down'}` }).catch(() => '');
|
|
54
66
|
await this.deps.rollback();
|
|
55
67
|
await this.deps.notify(`CRASH detected → rollback (report: ${reportPath}, error: ${health.error ?? 'down'})`).catch(() => { });
|
|
68
|
+
if (!process.env.VITEST) {
|
|
69
|
+
void runDebugAgent({ reportPath, health }).catch(() => { });
|
|
70
|
+
setTimeout(() => {
|
|
71
|
+
findInterrupted().then(r => {
|
|
72
|
+
if (r.interrupted.length)
|
|
73
|
+
this.deps.notify(`RESUME: ${r.interrupted.length} interrupted sessions`).catch(() => { });
|
|
74
|
+
}).catch(() => { });
|
|
75
|
+
}, 0);
|
|
76
|
+
}
|
|
56
77
|
}
|
|
57
78
|
finally {
|
|
58
79
|
this.rollingBack = false;
|