@openape/nest 1.0.0 → 1.1.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/dist/index.mjs +85 -94
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -95,19 +95,18 @@ async function handleAgentSpawn(ctx) {
|
|
|
95
95
|
} : void 0
|
|
96
96
|
};
|
|
97
97
|
upsertAgent(entry);
|
|
98
|
-
ctx.supervisor.reconcile(listAgents());
|
|
98
|
+
await ctx.supervisor.reconcile(listAgents());
|
|
99
99
|
return { name, email: entry.email, uid, home: entry.home };
|
|
100
100
|
}
|
|
101
101
|
async function handleAgentDestroy(ctx, name) {
|
|
102
102
|
if (!NAME_REGEX.test(name)) throw new Error(`invalid agent name "${name}"`);
|
|
103
103
|
const entry = findAgent(name);
|
|
104
104
|
if (!entry) throw new Error(`agent "${name}" not registered with this nest`);
|
|
105
|
-
ctx.supervisor.stop(name);
|
|
106
105
|
ctx.log(`nest: destroying agent "${name}"...`);
|
|
107
106
|
const args = ["run", "--as", "root", "--", "apes", "agents", "destroy", name, "--force"];
|
|
108
107
|
await execFileAsync(ctx.apesBin, args, { maxBuffer: 4 * 1024 * 1024 });
|
|
109
108
|
removeAgent(name);
|
|
110
|
-
ctx.supervisor.reconcile(listAgents());
|
|
109
|
+
await ctx.supervisor.reconcile(listAgents());
|
|
111
110
|
return { name, removed: true };
|
|
112
111
|
}
|
|
113
112
|
async function readUidFromDscl(name) {
|
|
@@ -233,109 +232,98 @@ function reapStaleResponses(log2) {
|
|
|
233
232
|
}
|
|
234
233
|
}
|
|
235
234
|
|
|
236
|
-
// src/lib/supervisor.ts
|
|
237
|
-
import {
|
|
235
|
+
// src/lib/pm2-supervisor.ts
|
|
236
|
+
import { execFile as execFile2 } from "child_process";
|
|
237
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
238
|
+
import { join as join3 } from "path";
|
|
238
239
|
import process from "process";
|
|
239
|
-
|
|
240
|
-
var
|
|
241
|
-
var
|
|
242
|
-
|
|
240
|
+
import { promisify as promisify2 } from "util";
|
|
241
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
242
|
+
var AGENTS_DIR = "/var/openape/agents";
|
|
243
|
+
function pm2AppName(agentName) {
|
|
244
|
+
return `openape-bridge-${agentName}`;
|
|
245
|
+
}
|
|
246
|
+
function ecosystemPath(agentName) {
|
|
247
|
+
return join3(AGENTS_DIR, agentName, "ecosystem.config.js");
|
|
248
|
+
}
|
|
249
|
+
function ecosystemContents(apesBin, agentName) {
|
|
250
|
+
void apesBin;
|
|
251
|
+
return `// Auto-generated by Pm2Supervisor for agent '${agentName}'.
|
|
252
|
+
// Edit at runtime via:
|
|
253
|
+
// apes run --as ${agentName} -- pm2 reload ${pm2AppName(agentName)}
|
|
254
|
+
module.exports = {
|
|
255
|
+
apps: [{
|
|
256
|
+
name: '${pm2AppName(agentName)}',
|
|
257
|
+
script: 'openape-chat-bridge',
|
|
258
|
+
autorestart: true,
|
|
259
|
+
max_restarts: 10,
|
|
260
|
+
min_uptime: '30s',
|
|
261
|
+
restart_delay: 2000,
|
|
262
|
+
merge_logs: true,
|
|
263
|
+
}],
|
|
264
|
+
}
|
|
265
|
+
`;
|
|
266
|
+
}
|
|
267
|
+
var Pm2Supervisor = class {
|
|
243
268
|
constructor(deps) {
|
|
244
269
|
this.deps = deps;
|
|
245
270
|
}
|
|
246
|
-
|
|
247
|
-
/** Bring
|
|
248
|
-
reconcile(desired) {
|
|
249
|
-
const desiredNames = new Set(desired.filter((a) => a.bridge != null).map((a) => a.name));
|
|
250
|
-
for (const [name] of this.children) {
|
|
251
|
-
if (!desiredNames.has(name)) this.stop(name);
|
|
252
|
-
}
|
|
271
|
+
inflight = /* @__PURE__ */ new Set();
|
|
272
|
+
/** Bring per-agent pm2 state in line with the registry. Idempotent. */
|
|
273
|
+
async reconcile(desired) {
|
|
253
274
|
for (const agent of desired) {
|
|
254
275
|
if (agent.bridge == null) continue;
|
|
255
|
-
if (
|
|
276
|
+
if (this.inflight.has(agent.name)) continue;
|
|
277
|
+
this.inflight.add(agent.name);
|
|
278
|
+
try {
|
|
279
|
+
await this.startOrReload(agent.name);
|
|
280
|
+
} catch (err) {
|
|
281
|
+
this.deps.log(`pm2-supervisor: ${agent.name} startOrReload failed: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`);
|
|
282
|
+
} finally {
|
|
283
|
+
this.inflight.delete(agent.name);
|
|
284
|
+
}
|
|
256
285
|
}
|
|
257
286
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
status() {
|
|
262
|
-
const now = Date.now();
|
|
263
|
-
return Array.from(this.children.entries()).map(([name, s]) => ({
|
|
264
|
-
name,
|
|
265
|
-
pid: s.child.pid ?? -1,
|
|
266
|
-
uptimeSec: Math.floor((now - s.startedAt) / 1e3),
|
|
267
|
-
consecutiveCrashes: s.consecutiveCrashes
|
|
268
|
-
}));
|
|
269
|
-
}
|
|
270
|
-
start(agent) {
|
|
271
|
-
if (this.children.has(agent.name)) return;
|
|
272
|
-
this.deps.log(`supervisor: starting bridge for ${agent.name}`);
|
|
273
|
-
this.spawnChild(agent, 0);
|
|
274
|
-
}
|
|
275
|
-
stop(name) {
|
|
276
|
-
const s = this.children.get(name);
|
|
277
|
-
if (!s) return;
|
|
278
|
-
this.deps.log(`supervisor: stopping bridge for ${name}`);
|
|
279
|
-
if (s.restartTimer) clearTimeout(s.restartTimer);
|
|
280
|
-
this.children.delete(name);
|
|
287
|
+
/** Stop one agent's pm2 app — used at destroy time. */
|
|
288
|
+
async stop(agentName) {
|
|
289
|
+
const name = pm2AppName(agentName);
|
|
281
290
|
try {
|
|
282
|
-
|
|
283
|
-
|
|
291
|
+
await this.runAsAgent(agentName, ["pm2", "delete", name]);
|
|
292
|
+
this.deps.log(`pm2-supervisor: deleted ${name}`);
|
|
293
|
+
} catch (err) {
|
|
294
|
+
this.deps.log(`pm2-supervisor: delete ${name}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`);
|
|
284
295
|
}
|
|
285
296
|
}
|
|
286
|
-
|
|
287
|
-
|
|
297
|
+
/** Best-effort cleanup — called on Nest shutdown. We don't kill
|
|
298
|
+
* the per-agent pm2-daemons; they should keep running so bridges
|
|
299
|
+
* stay alive across Nest restarts. No-op for now. */
|
|
300
|
+
async stopAll() {
|
|
288
301
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
// the nest identity for the YOLO grant).
|
|
298
|
-
env: process.env
|
|
299
|
-
});
|
|
300
|
-
child.stdout?.on("data", (chunk) => this.forwardLog(agent.name, "stdout", chunk));
|
|
301
|
-
child.stderr?.on("data", (chunk) => this.forwardLog(agent.name, "stderr", chunk));
|
|
302
|
-
const supervised = {
|
|
303
|
-
child,
|
|
304
|
-
consecutiveCrashes: prevCrashes,
|
|
305
|
-
startedAt: Date.now()
|
|
306
|
-
};
|
|
307
|
-
this.children.set(agent.name, supervised);
|
|
308
|
-
child.on("exit", (code, signal) => {
|
|
309
|
-
const stillManaged = this.children.get(agent.name) === supervised;
|
|
310
|
-
if (!stillManaged) return;
|
|
311
|
-
const ranLongEnough = Date.now() - supervised.startedAt > STABLE_RUNTIME_MS;
|
|
312
|
-
const nextCrashes = ranLongEnough ? 1 : prevCrashes + 1;
|
|
313
|
-
const backoff = Math.min(MAX_BACKOFF_MS, MIN_BACKOFF_MS * 2 ** Math.max(0, nextCrashes - 1));
|
|
314
|
-
this.deps.log(
|
|
315
|
-
`supervisor: ${agent.name} bridge exited code=${code} signal=${signal ?? "none"} consecutive=${nextCrashes} \u2192 respawn in ${backoff}ms`
|
|
316
|
-
);
|
|
317
|
-
supervised.restartTimer = setTimeout(() => {
|
|
318
|
-
if (this.children.get(agent.name) !== supervised) return;
|
|
319
|
-
this.children.delete(agent.name);
|
|
320
|
-
this.spawnChild(agent, nextCrashes);
|
|
321
|
-
}, backoff);
|
|
322
|
-
});
|
|
302
|
+
async startOrReload(agentName) {
|
|
303
|
+
mkdirSync3(AGENTS_DIR, { recursive: true, mode: 493 });
|
|
304
|
+
const dir = join3(AGENTS_DIR, agentName);
|
|
305
|
+
mkdirSync3(dir, { recursive: true, mode: 493 });
|
|
306
|
+
const path = ecosystemPath(agentName);
|
|
307
|
+
writeFileSync3(path, ecosystemContents(this.deps.apesBin, agentName), { mode: 420 });
|
|
308
|
+
await this.runAsAgent(agentName, ["pm2", "startOrReload", path]);
|
|
309
|
+
this.deps.log(`pm2-supervisor: ${agentName} bridge (re)started via pm2`);
|
|
323
310
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
311
|
+
/** Run a pm2 subcommand AS the agent — escapes-helper does the
|
|
312
|
+
* setuid switch, then exec's pm2 in the agent's uid. */
|
|
313
|
+
async runAsAgent(agentName, args) {
|
|
314
|
+
return execFileAsync2(
|
|
315
|
+
this.deps.apesBin,
|
|
316
|
+
["run", "--as", agentName, "--wait", "--", ...args],
|
|
317
|
+
{ maxBuffer: 1024 * 1024, env: process.env, timeout: 6e4 }
|
|
318
|
+
);
|
|
331
319
|
}
|
|
332
320
|
};
|
|
333
321
|
|
|
334
322
|
// src/lib/troop-sync.ts
|
|
335
|
-
import { execFile as
|
|
323
|
+
import { execFile as execFile3 } from "child_process";
|
|
336
324
|
import process2 from "process";
|
|
337
|
-
import { promisify as
|
|
338
|
-
var
|
|
325
|
+
import { promisify as promisify3 } from "util";
|
|
326
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
339
327
|
var TICK_MS = 5 * 60 * 1e3;
|
|
340
328
|
var TroopSync = class {
|
|
341
329
|
constructor(deps) {
|
|
@@ -369,7 +357,7 @@ var TroopSync = class {
|
|
|
369
357
|
}
|
|
370
358
|
async syncOne(name) {
|
|
371
359
|
try {
|
|
372
|
-
await
|
|
360
|
+
await execFileAsync3(
|
|
373
361
|
this.deps.apesBin,
|
|
374
362
|
["run", "--as", name, "--wait", "--", "apes", "agents", "sync"],
|
|
375
363
|
{ maxBuffer: 1024 * 1024, env: process2.env, timeout: 6e4 }
|
|
@@ -386,17 +374,20 @@ function log(line) {
|
|
|
386
374
|
process3.stderr.write(`${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
387
375
|
`);
|
|
388
376
|
}
|
|
389
|
-
var supervisor = new
|
|
377
|
+
var supervisor = new Pm2Supervisor({ apesBin: APES_BIN, log });
|
|
390
378
|
var troopSync = new TroopSync({ apesBin: APES_BIN, log });
|
|
391
379
|
var intentChannel = new IntentChannel({ apesBin: APES_BIN, supervisor, log });
|
|
392
|
-
supervisor.reconcile(listAgents())
|
|
393
|
-
log(
|
|
380
|
+
void supervisor.reconcile(listAgents()).then(
|
|
381
|
+
() => log("nest: pm2-supervisor reconciled with registry")
|
|
382
|
+
).catch((err) => {
|
|
383
|
+
log(`nest: pm2-supervisor reconcile failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
384
|
+
});
|
|
394
385
|
troopSync.start();
|
|
395
386
|
intentChannel.start();
|
|
396
387
|
var reaperTimer = setInterval(reapStaleResponses, 60 * 60 * 1e3, log);
|
|
397
388
|
process3.on("SIGTERM", () => {
|
|
398
389
|
log("nest: SIGTERM \u2014 stopping");
|
|
399
|
-
supervisor.stopAll();
|
|
390
|
+
void supervisor.stopAll();
|
|
400
391
|
troopSync.stop();
|
|
401
392
|
intentChannel.stop();
|
|
402
393
|
clearInterval(reaperTimer);
|
|
@@ -404,7 +395,7 @@ process3.on("SIGTERM", () => {
|
|
|
404
395
|
});
|
|
405
396
|
process3.on("SIGINT", () => {
|
|
406
397
|
log("nest: SIGINT \u2014 stopping");
|
|
407
|
-
supervisor.stopAll();
|
|
398
|
+
void supervisor.stopAll();
|
|
408
399
|
troopSync.stop();
|
|
409
400
|
intentChannel.stop();
|
|
410
401
|
clearInterval(reaperTimer);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openape/nest",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "OpenApe Nest — local control-plane daemon that supervises agent processes on this computer. Talks to troop SP for ownership state, spawns/destroys agents via DDISA always-grants, supervises chat-bridge children (replacing per-agent launchd plists).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|