Add resident Persona scheduler organ

This commit is contained in:
Meta Vens 2026-07-15 21:24:47 +01:00
commit 288781239c
5 changed files with 118 additions and 1 deletions

View file

@ -0,0 +1,11 @@
{
"name": "@voidbot/persona-scheduler",
"version": "0.1.0",
"private": true,
"main": "dist/index.js",
"types": "dist/index.d.ts",
"dependencies": {
"@voidbot/config": "0.1.0",
"dotenv": "^16.4.0"
}
}

View file

@ -0,0 +1,76 @@
import "dotenv/config";
import { spawn } from "node:child_process";
import { resolve } from "node:path";
import { loadConfig } from "@voidbot/config";
const config = loadConfig();
const intervalMs = Math.max(1, config.repoFaceHeartbeats.intervalMinutes) * 60_000;
const turnEnginePath = resolve(process.cwd(), "scripts", "run-repo-face-heartbeats.ts");
let stopping = false;
let activeTick: Promise<void> | undefined;
let timer: NodeJS.Timeout | undefined;
function runTick(): Promise<void> {
if (activeTick) {
console.warn("Persona scheduler skipped a pulse because the previous turn-selection tick is still active.");
return activeTick;
}
activeTick = new Promise<void>((done) => {
const startedAt = new Date().toISOString();
const child = spawn(process.execPath, [turnEnginePath], {
cwd: process.cwd(),
env: process.env,
stdio: ["ignore", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
child.stdout.setEncoding("utf8");
child.stderr.setEncoding("utf8");
child.stdout.on("data", (chunk: string) => { stdout += chunk; });
child.stderr.on("data", (chunk: string) => { stderr += chunk; });
child.once("error", (error) => {
console.error(`Persona scheduler tick failed to start: ${error.message}`);
});
child.once("close", (code, signal) => {
const output = stdout.trim();
if (code === 0) {
console.log(`Persona scheduler tick completed (started ${startedAt}).${output ? ` ${output}` : ""}`);
} else {
console.error(
`Persona scheduler tick failed (started ${startedAt}, code ${String(code)}, signal ${String(signal)}).${stderr.trim() ? ` ${stderr.trim()}` : ""}`,
);
}
done();
});
}).finally(() => {
activeTick = undefined;
});
return activeTick;
}
function scheduleNext(): void {
if (stopping) return;
timer = setTimeout(async () => {
await runTick();
scheduleNext();
}, intervalMs);
timer.unref();
}
async function shutdown(signal: string): Promise<void> {
if (stopping) return;
stopping = true;
if (timer) clearTimeout(timer);
console.log(`Persona scheduler stopping after ${signal}.`);
await activeTick;
}
process.once("SIGINT", () => { void shutdown("SIGINT"); });
process.once("SIGTERM", () => { void shutdown("SIGTERM"); });
console.log(`Persona scheduler owns the resident ${config.repoFaceHeartbeats.intervalMinutes}-minute turn pulse.`);
void runTick().then(scheduleNext);

View file

@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "dist"
},
"include": [
"src/**/*.ts"
],
"references": [
{
"path": "../../packages/config"
}
]
}

12
package-lock.json generated
View file

@ -33,6 +33,14 @@
"ffmpeg-static": "^5.3.0"
}
},
"apps/persona-scheduler": {
"name": "@voidbot/persona-scheduler",
"version": "0.1.0",
"dependencies": {
"@voidbot/config": "0.1.0",
"dotenv": "^16.4.0"
}
},
"apps/worker": {
"name": "@voidbot/worker",
"version": "0.1.0",
@ -1163,6 +1171,10 @@
"resolved": "packages/core",
"link": true
},
"node_modules/@voidbot/persona-scheduler": {
"resolved": "apps/persona-scheduler",
"link": true
},
"node_modules/@voidbot/providers": {
"resolved": "packages/providers",
"link": true

View file

@ -24,7 +24,9 @@
},
{
"path": "./apps/worker"
},
{
"path": "./apps/persona-scheduler"
}
]
}