@hamp10/agentforge 0.1.0 → 0.2.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/bin/agentforge.js +390 -44
- package/package.json +1 -1
- package/src/OpenClawCLI.js +204 -46
- package/src/resolveOpenclaw.js +105 -0
- package/src/selfUpdate.js +66 -0
- package/src/supervisor.js +128 -0
- package/src/worker.js +265 -227
- package/templates/agent/AGENTFORGE.md +148 -56
- package/templates/agent/AGENTS.md +0 -212
- package/templates/agent/SOUL.md +0 -36
- package/templates/agent/TOOLS.md +0 -40
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supervisor: spawns the worker process and restarts it on crash.
|
|
3
|
+
*
|
|
4
|
+
* Usage modes (controlled by bin/agentforge.js):
|
|
5
|
+
* agentforge start — foreground supervisor (Ctrl+C stops everything)
|
|
6
|
+
* agentforge start --detach — background supervisor (shell returns immediately)
|
|
7
|
+
* agentforge start --no-daemon — raw worker, no supervision (internal use only)
|
|
8
|
+
*
|
|
9
|
+
* The supervisor re-execs this same binary with --no-daemon so the actual
|
|
10
|
+
* worker code path is unchanged — only the lifecycle wrapper is new.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { spawn } from 'child_process';
|
|
14
|
+
import { mkdirSync, writeFileSync, readFileSync, unlinkSync, existsSync } from 'fs';
|
|
15
|
+
import { homedir } from 'os';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
|
|
18
|
+
const BACKOFF_MS = [1000, 2000, 5000, 15000, 30000];
|
|
19
|
+
const CRASH_WINDOW = 10_000; // process lived < 10s → count as crash
|
|
20
|
+
const CONFIG_DIR = path.join(homedir(), '.agentforge');
|
|
21
|
+
const PID_FILE = path.join(CONFIG_DIR, 'supervisor.pid');
|
|
22
|
+
const WORKER_PID_FILE = path.join(CONFIG_DIR, 'worker.pid');
|
|
23
|
+
|
|
24
|
+
function writePid(file, pid) {
|
|
25
|
+
try {
|
|
26
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
27
|
+
writeFileSync(file, String(pid));
|
|
28
|
+
} catch {}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function removePid(file) {
|
|
32
|
+
try { unlinkSync(file); } catch {}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Run the supervisor in the foreground. Blocks until a clean exit (code 0).
|
|
37
|
+
* @param {string[]} innerArgv argv to pass to the worker process (must include --no-daemon)
|
|
38
|
+
*/
|
|
39
|
+
export async function runSupervisor(innerArgv) {
|
|
40
|
+
writePid(PID_FILE, process.pid);
|
|
41
|
+
|
|
42
|
+
process.on('SIGTERM', () => {
|
|
43
|
+
console.log('[supervisor] Received SIGTERM — shutting down');
|
|
44
|
+
removePid(PID_FILE);
|
|
45
|
+
process.exit(0);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
let consecutiveCrashes = 0;
|
|
49
|
+
|
|
50
|
+
while (true) {
|
|
51
|
+
const startTime = Date.now();
|
|
52
|
+
|
|
53
|
+
const child = spawn(process.execPath, [process.argv[1], ...innerArgv], {
|
|
54
|
+
stdio: 'inherit',
|
|
55
|
+
detached: false,
|
|
56
|
+
env: { ...process.env, AGENTFORGE_SKIP_UPDATE: '1' },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
writePid(WORKER_PID_FILE, child.pid);
|
|
60
|
+
|
|
61
|
+
const exitCode = await new Promise(resolve => {
|
|
62
|
+
child.on('exit', resolve);
|
|
63
|
+
child.on('error', () => resolve(1));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
removePid(WORKER_PID_FILE);
|
|
67
|
+
|
|
68
|
+
// Clean exit (0) = user Ctrl+C or graceful shutdown — stop supervising
|
|
69
|
+
if (exitCode === 0) {
|
|
70
|
+
removePid(PID_FILE);
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const elapsed = Date.now() - startTime;
|
|
75
|
+
if (elapsed < CRASH_WINDOW) {
|
|
76
|
+
consecutiveCrashes++;
|
|
77
|
+
} else {
|
|
78
|
+
consecutiveCrashes = 0; // ran long enough — reset crash counter
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const backoff = BACKOFF_MS[Math.min(consecutiveCrashes - 1, BACKOFF_MS.length - 1)] ?? BACKOFF_MS[0];
|
|
82
|
+
console.log(`\n[supervisor] Worker exited (code ${exitCode ?? 'null'}), crash #${consecutiveCrashes} — restarting in ${backoff}ms...`);
|
|
83
|
+
await new Promise(r => setTimeout(r, backoff));
|
|
84
|
+
console.log('[supervisor] Restarting worker...\n');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Detach: spawn this binary as a background supervisor, then return control
|
|
90
|
+
* to the shell immediately. The background process runs runSupervisor().
|
|
91
|
+
* @param {string[]} supervisorArgv argv for the background supervisor (without --detach)
|
|
92
|
+
*/
|
|
93
|
+
export function detachSupervisor(supervisorArgv) {
|
|
94
|
+
const logFile = path.join(CONFIG_DIR, 'worker.log');
|
|
95
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
96
|
+
|
|
97
|
+
const child = spawn(process.execPath, [process.argv[1], ...supervisorArgv], {
|
|
98
|
+
detached: true,
|
|
99
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
100
|
+
env: { ...process.env, AGENTFORGE_SKIP_UPDATE: '1' },
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
child.unref();
|
|
104
|
+
|
|
105
|
+
writePid(PID_FILE, child.pid);
|
|
106
|
+
console.log(`✅ AgentForge worker started in background (PID ${child.pid})`);
|
|
107
|
+
console.log(` Logs: ${logFile}`);
|
|
108
|
+
console.log(` Stop: agentforge stop`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Stop a running background supervisor by sending SIGTERM to the PID in the pid file.
|
|
113
|
+
*/
|
|
114
|
+
export function stopSupervisor() {
|
|
115
|
+
if (!existsSync(PID_FILE)) {
|
|
116
|
+
console.log('No running supervisor found.');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const pid = parseInt(readFileSync(PID_FILE, 'utf8'));
|
|
121
|
+
process.kill(pid, 'SIGTERM');
|
|
122
|
+
removePid(PID_FILE);
|
|
123
|
+
console.log(`Stopped supervisor (PID ${pid})`);
|
|
124
|
+
} catch (e) {
|
|
125
|
+
console.log('Supervisor not running or already stopped.');
|
|
126
|
+
removePid(PID_FILE);
|
|
127
|
+
}
|
|
128
|
+
}
|