Cut Persona scheduling from local orchestration debris

This commit is contained in:
Meta Vens 2026-07-15 21:45:57 +01:00
commit d2d71febd9
11 changed files with 21 additions and 290 deletions

View file

@ -1,87 +0,0 @@
param(
[string] $TaskName,
[int] $IntervalMinutes,
[switch] $RunNow
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
$repoRoot = Split-Path -Parent $PSScriptRoot
$envFile = Join-Path $repoRoot ".env"
$runnerScript = Join-Path $PSScriptRoot "run-repo-face-heartbeats.ts"
$runnerWrapper = Join-Path $PSScriptRoot "run-repo-face-heartbeats.ps1"
$hiddenLauncher = Join-Path $PSScriptRoot "run-hidden-powershell.vbs"
function Read-DotEnv {
param([Parameter(Mandatory = $true)][string] $Path)
$values = @{}
if (-not (Test-Path $Path)) {
return $values
}
foreach ($line in Get-Content -Path $Path) {
if ([string]::IsNullOrWhiteSpace($line) -or $line.TrimStart().StartsWith("#")) {
continue
}
$separatorIndex = $line.IndexOf("=")
if ($separatorIndex -lt 1) {
continue
}
$values[$line.Substring(0, $separatorIndex).Trim()] = $line.Substring($separatorIndex + 1)
}
return $values
}
$config = Read-DotEnv -Path $envFile
$taskNameValue = if (-not [string]::IsNullOrWhiteSpace($TaskName)) {
$TaskName
} elseif ($config.ContainsKey("REPO_FACE_HEARTBEAT_TASK_NAME") -and -not [string]::IsNullOrWhiteSpace($config["REPO_FACE_HEARTBEAT_TASK_NAME"])) {
$config["REPO_FACE_HEARTBEAT_TASK_NAME"]
} else {
"VoidBot Repo Face Heartbeats"
}
$intervalMinutesValue = if ($PSBoundParameters.ContainsKey("IntervalMinutes")) {
$IntervalMinutes
} elseif ($config.ContainsKey("REPO_FACE_HEARTBEAT_INTERVAL_MINUTES") -and -not [string]::IsNullOrWhiteSpace($config["REPO_FACE_HEARTBEAT_INTERVAL_MINUTES"])) {
[int]$config["REPO_FACE_HEARTBEAT_INTERVAL_MINUTES"]
} else {
15
}
if ($intervalMinutesValue -lt 1) {
throw "REPO_FACE_HEARTBEAT_INTERVAL_MINUTES must be at least 1."
}
$tsxCliPath = Join-Path $repoRoot "node_modules\tsx\dist\cli.mjs"
if (-not (Test-Path -LiteralPath $tsxCliPath)) {
throw "Missing tsx CLI at $tsxCliPath. Run npm install first."
}
if (-not (Test-Path -LiteralPath $runnerWrapper)) {
throw "Missing repo Face heartbeat PowerShell wrapper at $runnerWrapper."
}
if (-not (Test-Path -LiteralPath $hiddenLauncher)) {
throw "Missing hidden PowerShell launcher at $hiddenLauncher."
}
$startAt = (Get-Date).AddMinutes(3)
$action = New-ScheduledTaskAction `
-Execute "wscript.exe" `
-Argument "//B //nologo `"$hiddenLauncher`" -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$runnerWrapper`"" `
-WorkingDirectory $repoRoot
$trigger = New-ScheduledTaskTrigger -Once -At $startAt -RepetitionInterval (New-TimeSpan -Minutes $intervalMinutesValue) -RepetitionDuration (New-TimeSpan -Days 3650)
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit (New-TimeSpan -Minutes 10) -MultipleInstances IgnoreNew
$principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" -LogonType Interactive -RunLevel Limited
$description = "Runs VoidBot's repo Face initiative scheduler every $intervalMinutesValue minute(s)."
Register-ScheduledTask -TaskName $taskNameValue -Action $action -Trigger $trigger -Settings $settings -Principal $principal -Description $description -Force | Out-Null
Write-Host "Installed scheduled task: $taskNameValue"
Write-Host "Schedule: every $intervalMinutesValue minute(s), starting at $($startAt.ToString('yyyy-MM-dd HH:mm'))"
Write-Host "Working directory: $repoRoot"
Write-Host "Runner script: $runnerScript"
if ($RunNow) {
Start-ScheduledTask -TaskName $taskNameValue
Write-Host "Started task immediately."
}

View file

@ -478,7 +478,6 @@ try {
$agentSwarmPaused = Test-AgentSwarmPaused
$agentSwarmOrganIds = @{
"bifrost-dispatch" = $true
"repo-face-heartbeats" = $true
"void-mood-drift" = $true
"void-moderation-heartbeat" = $true
"void-moderation-rumination" = $true
@ -511,15 +510,6 @@ try {
Executable = $node
Arguments = @((Join-Path $PSScriptRoot "render-voidbot-swarm-dashboard.mjs"))
},
[pscustomobject]@{
Id = "repo-face-heartbeats"
Label = "Repo Face CTB turns"
IntervalMinutes = Get-ConfigInt -Config $config -Name "REPO_FACE_HEARTBEAT_INTERVAL_MINUTES" -Default 1 -Minimum 1
TimeoutMinutes = 20
Cwd = $repoRoot
Executable = $powershell
Arguments = @("-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", (Join-Path $PSScriptRoot "run-repo-face-heartbeats.ps1"))
},
[pscustomobject]@{
Id = "void-mood-drift"
Label = "Void mood drift"

View file

@ -1,23 +0,0 @@
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
$repoRoot = Split-Path -Parent $PSScriptRoot
$runnerScript = Join-Path $PSScriptRoot "run-repo-face-heartbeats.ts"
$tsxCliPath = Join-Path $repoRoot "node_modules\tsx\dist\cli.mjs"
$nodePath = (Get-Command node.exe -ErrorAction Stop).Source
if (-not (Test-Path -LiteralPath $tsxCliPath)) {
throw "Missing tsx CLI at $tsxCliPath. Run npm install first."
}
if (-not (Test-Path -LiteralPath $runnerScript)) {
throw "Missing repo Face heartbeat runner at $runnerScript."
}
Push-Location $repoRoot
try {
& $nodePath $tsxCliPath $runnerScript
exit $LASTEXITCODE
} finally {
Pop-Location
}

View file

@ -730,25 +730,6 @@ async function loadRepoFaceRestStates(
},
);
}
const maintenance = options.dryRun
? { started: false, reason: "dry_run", statusPath: undefined }
: await maybeStartRepoFaceMemoryMaintenance({
identity,
statePath,
typedState,
projectedRest: projected,
});
if (maintenance.started || maintenance.failureReason) {
heartbeatState.history.push({
type: maintenance.started ? "repo_face_memory_maintenance_started" : "repo_face_memory_maintenance_failed_to_start",
identityId: identity.id,
observedAt: now.toISOString(),
reason: maintenance.reason,
statusPath: maintenance.statusPath,
pid: maintenance.pid,
failureReason: maintenance.failureReason,
});
}
restStates.set(identity.id, {
isNapping: projected.isNapping,
napEndsAt: projected.napEndsAt,
@ -863,144 +844,6 @@ function speakingPressuresEqual(
&& left.lastSpokeAt === right.lastSpokeAt;
}
async function maybeStartRepoFaceMemoryMaintenance(input: {
identity: RepoDiscordIdentity;
statePath: string;
typedState: Awaited<ReturnType<typeof loadVoidSelfStateTypedDocuments>>;
projectedRest: RepoFaceRestSnapshot;
}): Promise<{
started: boolean;
reason?: string;
statusPath?: string;
pid?: number;
failureReason?: string;
}> {
const shortTerm = input.typedState.thoughtMemory.shortTerm ?? [];
if (shortTerm.length === 0) {
return { started: false, reason: "no_short_term_memory" };
}
const oldestShortTermMs = Math.min(
...shortTerm.map((memory) => Date.parse(memory.updatedAt)).filter(Number.isFinite),
);
const hasStaleShortTerm =
Number.isFinite(oldestShortTermMs) &&
Date.now() - oldestShortTermMs >= 6 * 60 * 60 * 1000;
if (input.projectedRest.isNapping !== true && !hasStaleShortTerm) {
return { started: false, reason: "not_napping_or_stale" };
}
const paths = repoFaceMemoryMaintenancePaths(input.statePath);
const currentStatus = await readJsonFile(paths.statusPath);
if (isRecentRunningStatus(currentStatus, 25)) {
return { started: false, reason: "maintenance_already_running", statusPath: paths.statusPath };
}
if (isStatusCompletedAfter(currentStatus, newestTimestampMs(shortTerm))) {
return { started: false, reason: "maintenance_already_completed_for_sources", statusPath: paths.statusPath };
}
try {
await mkdir(paths.statusDir, { recursive: true });
await mkdir(paths.logDir, { recursive: true });
const launchedAt = new Date();
await writeJsonFile(paths.statusPath, {
status: "starting",
startedAt: launchedAt.toISOString(),
stateFile: input.statePath,
launcher: "repo_face_heartbeat",
reason: input.projectedRest.isNapping ? "repo_face_napping_with_short_term_memory" : "repo_face_stale_short_term_memory",
});
const child = spawn(
"powershell.exe",
[
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-File",
resolve(process.cwd(), "scripts", "run-void-memory-maintenance.ps1"),
"-StateFilePath",
input.statePath,
"-ForceDistillation",
],
{
cwd: process.cwd(),
detached: true,
stdio: "ignore",
windowsHide: true,
env: {
...process.env,
VOID_MEMORY_MAINTENANCE_STATUS_DIR: paths.statusDir,
VOID_MEMORY_MAINTENANCE_LOG_DIR: paths.logDir,
},
},
);
child.unref();
return {
started: true,
reason: input.projectedRest.isNapping ? "repo_face_napping_with_short_term_memory" : "repo_face_stale_short_term_memory",
statusPath: paths.statusPath,
pid: child.pid,
};
} catch (error) {
return {
started: false,
reason: "spawn_failed",
statusPath: paths.statusPath,
failureReason: error instanceof Error ? error.message : String(error),
};
}
}
function repoFaceMemoryMaintenancePaths(statePath: string): {
statusDir: string;
logDir: string;
statusPath: string;
} {
const stateDir = dirname(statePath);
const voidbotRoot = dirname(stateDir);
const statusDir = resolve(voidbotRoot, "status");
const logDir = resolve(voidbotRoot, "logs");
return {
statusDir,
logDir,
statusPath: resolve(statusDir, "void-memory-maintenance.json"),
};
}
async function readJsonFile(path: string): Promise<Record<string, unknown> | undefined> {
try {
return JSON.parse(stripLeadingBom(await readFile(path, "utf8"))) as Record<string, unknown>;
} catch {
return undefined;
}
}
async function writeJsonFile(path: string, value: unknown): Promise<void> {
await mkdir(dirname(path), { recursive: true });
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
}
function isRecentRunningStatus(status: Record<string, unknown> | undefined, maxAgeMinutes: number): boolean {
if (status?.status !== "running" && status?.status !== "running_skip_model" && status?.status !== "starting") {
return false;
}
const startedAtMs = typeof status.startedAt === "string" ? Date.parse(status.startedAt) : NaN;
return Number.isFinite(startedAtMs) && Date.now() - startedAtMs < maxAgeMinutes * 60_000;
}
function isStatusCompletedAfter(status: Record<string, unknown> | undefined, sourceTimestampMs: number): boolean {
if (status?.status !== "ok" || !Number.isFinite(sourceTimestampMs)) {
return false;
}
const finishedAtMs = typeof status.finishedAt === "string" ? Date.parse(status.finishedAt) : NaN;
return Number.isFinite(finishedAtMs) && finishedAtMs >= sourceTimestampMs;
}
function newestTimestampMs(entries: Array<{ updatedAt: string }>): number {
return Math.max(...entries.map((entry) => Date.parse(entry.updatedAt)).filter(Number.isFinite));
}
function sleepCyclesEqual(left: unknown, right: unknown): boolean {
return JSON.stringify(left) === JSON.stringify(right);
}