From 288781239c2dd3e9d9fcdbf087c58c2153d38548 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 21:24:47 +0100 Subject: [PATCH] Add resident Persona scheduler organ --- apps/persona-scheduler/package.json | 11 ++++ apps/persona-scheduler/src/index.ts | 76 ++++++++++++++++++++++++++++ apps/persona-scheduler/tsconfig.json | 16 ++++++ package-lock.json | 12 +++++ tsconfig.json | 4 +- 5 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 apps/persona-scheduler/package.json create mode 100644 apps/persona-scheduler/src/index.ts create mode 100644 apps/persona-scheduler/tsconfig.json diff --git a/apps/persona-scheduler/package.json b/apps/persona-scheduler/package.json new file mode 100644 index 0000000..33f61b1 --- /dev/null +++ b/apps/persona-scheduler/package.json @@ -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" + } +} diff --git a/apps/persona-scheduler/src/index.ts b/apps/persona-scheduler/src/index.ts new file mode 100644 index 0000000..00e53e1 --- /dev/null +++ b/apps/persona-scheduler/src/index.ts @@ -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 | undefined; +let timer: NodeJS.Timeout | undefined; + +function runTick(): Promise { + if (activeTick) { + console.warn("Persona scheduler skipped a pulse because the previous turn-selection tick is still active."); + return activeTick; + } + + activeTick = new Promise((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 { + 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); diff --git a/apps/persona-scheduler/tsconfig.json b/apps/persona-scheduler/tsconfig.json new file mode 100644 index 0000000..9f925df --- /dev/null +++ b/apps/persona-scheduler/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "rootDir": "src", + "outDir": "dist" + }, + "include": [ + "src/**/*.ts" + ], + "references": [ + { + "path": "../../packages/config" + } + ] +} diff --git a/package-lock.json b/package-lock.json index c5c7967..5a35149 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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 diff --git a/tsconfig.json b/tsconfig.json index 36c744e..cef896a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,7 +24,9 @@ }, { "path": "./apps/worker" + }, + { + "path": "./apps/persona-scheduler" } ] } -