Cut Persona scheduling from local orchestration debris
This commit is contained in:
parent
e83111f7d5
commit
d2d71febd9
11 changed files with 21 additions and 290 deletions
|
|
@ -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."
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue