diff --git a/.env.example b/.env.example index 05908c2..760eddd 100644 --- a/.env.example +++ b/.env.example @@ -68,5 +68,6 @@ OFFSITE_BACKUP_TASK_TIME=04:30 VOIDBOT_BACKUP_MAX_AGE_HOURS=30 VOIDBOT_OFFSITE_BACKUP_MAX_AGE_HOURS=36 VOIDBOT_HEALTHCHECK_NOTIFY_REPEAT_HOURS=12 +VOIDBOT_STACK_STARTUP_TASK_NAME=VoidBot Stack Startup VOIDBOT_HEALTHCHECK_TASK_NAME=VoidBot Operations Watchdog VOIDBOT_HEALTHCHECK_INTERVAL_MINUTES=60 diff --git a/README.md b/README.md index 5e4c373..0a0fcfe 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,14 @@ That script: - writes runtime status to `.voidbot/status/runtime-stack.json` - writes logs to `.voidbot/logs/bot.log` and `.voidbot/logs/worker.log` +If you do not want a reboot or fresh logon to leave the bot dead on the floor, install the startup task once: + +```bash +npm run state:install-startup-task +``` + +That creates the `VoidBot Stack Startup` scheduled task, which runs `scripts/start-voidbot-stack.ps1` at logon through the same hidden launcher shim the other ops tasks use. + If you want the raw split-terminal path for debugging, those still exist: ```bash diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 13bfa25..df66014 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -26,6 +26,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou - Interaction-memory logic is no longer one thousand-line slab; event/tone analysis, shared constants, and profile synthesis are split across `packages/core/src/interaction-memory-*.ts`. - State storage is no longer one persistence omnibus; `packages/core/src/state-storage.ts` is now a thin factory over domain-specific Postgres/file store modules plus bootstrap/migration helpers. - Ops health, backup verification, offsite sync, and dashboard surfaces already exist. +- The stack has a dedicated logon startup task path now, so a reboot does not have to mean manual necromancy for bot and worker. - The operations watchdog now supports ignored local extension checks, so adjacent-service fire signals can ride the same owner DM alert path without hardcoding private project gossip into the public repo. - The new state/notes surfaces are now the continuity spine for future nontrivial work. diff --git a/notes/voidbot-current-system-map.md b/notes/voidbot-current-system-map.md index da09a19..28c2b59 100644 --- a/notes/voidbot-current-system-map.md +++ b/notes/voidbot-current-system-map.md @@ -85,6 +85,8 @@ This note is the source-grounded description of how the live VoidBot stack is sh - `scripts/start-voidbot-stack.ps1` - stack bootstrap, health checks, fresh build, stale-process cleanup, bot/worker restart, runtime status emission. +- `scripts/install-stack-startup-task.ps1` + - installs the hidden logon task that runs `start-voidbot-stack.ps1` automatically after reboot or sign-in. - `scripts/check-voidbot-operations.ps1` - watchdog for process liveness, Qdrant, Postgres, Ollama, Discord auth, backup freshness, offsite sync freshness, and optional ignored local extension checks. - `scripts/backup-voidbot-state.ps1` diff --git a/notes/voidbot-implementation-plan.md b/notes/voidbot-implementation-plan.md index 623df96..bfa1dea 100644 --- a/notes/voidbot-implementation-plan.md +++ b/notes/voidbot-implementation-plan.md @@ -18,6 +18,7 @@ Use the persistent project-state surfaces to keep future work source-grounded an - add a real restore-drill cadence instead of stopping at backup verification - reduce dependence on interactive workstation logon where practical +- keep the stack startup task installed and visible so reboots do not quietly leave runtime dead - keep watchdog signals and dashboard diagnostics honest when dependencies are degraded - keep adjacent-service checks behind private local extensions so VoidBot does not quietly mutate into a generic infra panic funnel diff --git a/package.json b/package.json index 39bb25c..6cf31e4 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "state:dashboard": "powershell -ExecutionPolicy Bypass -File scripts/render-voidbot-operations-dashboard.ps1 -Open", "state:render-dashboard": "powershell -ExecutionPolicy Bypass -File scripts/render-voidbot-operations-dashboard.ps1", "state:sync-offsite": "powershell -ExecutionPolicy Bypass -File scripts/sync-voidbot-backup-offsite.ps1", + "state:install-startup-task": "powershell -ExecutionPolicy Bypass -File scripts/install-stack-startup-task.ps1", "state:install-offsite-task": "powershell -ExecutionPolicy Bypass -File scripts/install-offsite-backup-task.ps1", "state:install-watchdog-task": "powershell -ExecutionPolicy Bypass -File scripts/install-operations-watchdog-task.ps1", "postgres:up": "docker compose -f infra/postgres/docker-compose.yml up -d", diff --git a/scripts/check-voidbot-operations.ps1 b/scripts/check-voidbot-operations.ps1 index adcf2d8..2ba0f8e 100644 --- a/scripts/check-voidbot-operations.ps1 +++ b/scripts/check-voidbot-operations.ps1 @@ -546,6 +546,7 @@ $backupRoot = Join-Path $storageRoot "backups" $backupMaxAgeHours = if ($config.ContainsKey("VOIDBOT_BACKUP_MAX_AGE_HOURS")) { [double]$config["VOIDBOT_BACKUP_MAX_AGE_HOURS"] } else { 30.0 } $offsiteMaxAgeHours = if ($config.ContainsKey("VOIDBOT_OFFSITE_BACKUP_MAX_AGE_HOURS")) { [double]$config["VOIDBOT_OFFSITE_BACKUP_MAX_AGE_HOURS"] } else { 36.0 } $notifyRepeatHours = if ($config.ContainsKey("VOIDBOT_HEALTHCHECK_NOTIFY_REPEAT_HOURS")) { [double]$config["VOIDBOT_HEALTHCHECK_NOTIFY_REPEAT_HOURS"] } else { 12.0 } +$stackStartupTaskName = if ($config.ContainsKey("VOIDBOT_STACK_STARTUP_TASK_NAME") -and -not [string]::IsNullOrWhiteSpace($config["VOIDBOT_STACK_STARTUP_TASK_NAME"])) { $config["VOIDBOT_STACK_STARTUP_TASK_NAME"] } else { "VoidBot Stack Startup" } $watchdogTaskName = if ($config.ContainsKey("VOIDBOT_HEALTHCHECK_TASK_NAME") -and -not [string]::IsNullOrWhiteSpace($config["VOIDBOT_HEALTHCHECK_TASK_NAME"])) { $config["VOIDBOT_HEALTHCHECK_TASK_NAME"] } else { "VoidBot Operations Watchdog" } $offsiteTaskName = if ($config.ContainsKey("OFFSITE_BACKUP_TASK_NAME") -and -not [string]::IsNullOrWhiteSpace($config["OFFSITE_BACKUP_TASK_NAME"])) { $config["OFFSITE_BACKUP_TASK_NAME"] } else { "VoidBot Offsite Backup" } $offsiteRemoteWindowsDir = if ($config.ContainsKey("OFFSITE_BACKUP_REMOTE_WINDOWS_DIR") -and -not [string]::IsNullOrWhiteSpace($config["OFFSITE_BACKUP_REMOTE_WINDOWS_DIR"])) { $config["OFFSITE_BACKUP_REMOTE_WINDOWS_DIR"] } else { "C:\Meta\voidbot-backups" } @@ -865,6 +866,19 @@ try { } Set-WatchdogStep -CurrentStep "scheduled_task_checks" -Detail "Inspecting scheduled task installation and status files." + try { + $startupTask = Get-ScheduledTask -TaskName $stackStartupTaskName -ErrorAction Stop + $startupEnabled = [bool]$startupTask.Settings.Enabled + $startupTaskStatus = if ($startupEnabled) { "passed" } else { "failed" } + $startupTaskDetail = if ($startupEnabled) { "Stack startup task is installed and enabled." } else { "Stack startup task is installed but disabled." } + Add-Check -Name "scheduled_task.stack_startup" -Status $startupTaskStatus -Detail $startupTaskDetail -Data @{ + taskName = $stackStartupTaskName + state = [string]$startupTask.State + } + } catch { + Add-Check -Name "scheduled_task.stack_startup" -Status "warning" -Detail "Stack startup task $stackStartupTaskName is not installed yet." + } + try { $offsiteTask = Get-ScheduledTask -TaskName $offsiteTaskName -ErrorAction Stop $offsiteEnabled = [bool]$offsiteTask.Settings.Enabled diff --git a/scripts/install-stack-startup-task.ps1 b/scripts/install-stack-startup-task.ps1 new file mode 100644 index 0000000..9718e03 --- /dev/null +++ b/scripts/install-stack-startup-task.ps1 @@ -0,0 +1,77 @@ +param( + [string] $TaskName, + [switch] $RunNow +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" +$ProgressPreference = "SilentlyContinue" + +$repoRoot = Split-Path -Parent $PSScriptRoot +$envFile = Join-Path $repoRoot ".env" +$startScript = Join-Path $PSScriptRoot "start-voidbot-stack.ps1" +$hiddenLauncher = Join-Path $PSScriptRoot "run-hidden-powershell.vbs" + +function Read-DotEnv { + param( + [Parameter(Mandatory = $true)] + [string] $Path + ) + + if (-not (Test-Path $Path)) { + throw "Missing .env at $Path" + } + + $values = @{} + + foreach ($line in Get-Content -Path $Path) { + if ([string]::IsNullOrWhiteSpace($line)) { + continue + } + + if ($line.TrimStart().StartsWith("#")) { + continue + } + + $separatorIndex = $line.IndexOf("=") + + if ($separatorIndex -lt 1) { + continue + } + + $key = $line.Substring(0, $separatorIndex).Trim() + $value = $line.Substring($separatorIndex + 1) + $values[$key] = $value + } + + return $values +} + +$config = Read-DotEnv -Path $envFile +$taskNameValue = if (-not [string]::IsNullOrWhiteSpace($TaskName)) { + $TaskName +} elseif ($config.ContainsKey("VOIDBOT_STACK_STARTUP_TASK_NAME") -and -not [string]::IsNullOrWhiteSpace($config["VOIDBOT_STACK_STARTUP_TASK_NAME"])) { + $config["VOIDBOT_STACK_STARTUP_TASK_NAME"] +} else { + "VoidBot Stack Startup" +} + +$action = New-ScheduledTaskAction -Execute "wscript.exe" -Argument "//B //nologo `"$hiddenLauncher`" -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$startScript`"" +$trigger = New-ScheduledTaskTrigger -AtLogOn -User "$env:USERDOMAIN\$env:USERNAME" +$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit (New-TimeSpan -Minutes 30) +$principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" -LogonType Interactive -RunLevel Limited +$description = "Starts the VoidBot stack when this workstation user logs on so bot and worker do not stay dead after a reboot." + +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 "Trigger: at logon for $($env:USERDOMAIN)\$($env:USERNAME)" +Write-Host "Task runs as: $($env:USERDOMAIN)\$($env:USERNAME)" +Write-Host "Logon mode: Interactive" +Write-Host "Launcher: wscript.exe hidden PowerShell shim" +Write-Host "Execution time limit: 30 minutes" + +if ($RunNow) { + Start-ScheduledTask -TaskName $taskNameValue + Write-Host "Started task immediately." +} diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 4502108..44912be 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -8,3 +8,4 @@ {"ts":"2026-04-29T17:36:43Z","type":"implementation","status":"ok","note":"Operations watchdog now checks Repixelizer public health plus recent Yggdrasil journald pressure signals and routes queue-full, job-failed, stale-cancel, and slow-job issues through the existing owner DM notification path."} {"ts":"2026-04-29T18:20:00+01:00","type":"architecture","status":"ok","note":"Adjacent-service fire signals now load through an ignored local watchdog extension script instead of living in tracked VoidBot code, so the owner alert path stays useful without baking private cross-project monitoring into the public repo."} {"ts":"2026-04-29T19:06:00+01:00","type":"implementation","status":"ok","note":"The watchdog owner-DM path was failing on Windows because Discord JSON bodies were being shoved through curl command-line quoting and arriving malformed; writing the JSON to a temp file and posting it with --data-binary fixed the alert path."} +{"ts":"2026-04-30T00:00:00+01:00","type":"operations","status":"ok","note":"A reboot proved bot and worker would stay dead without a dedicated startup task; the stack now has a logon task installer and the watchdog checks that the startup task remains installed and enabled."} diff --git a/state/map.yaml b/state/map.yaml index 566ce7d..de72209 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -54,7 +54,7 @@ data_flow: active_subgoals: - Make the new project-state surfaces the default continuity path for nontrivial VoidBot work. - Keep public-lane limits, moderation, and provider behavior constrained enough that one 4060 Ti box does not get flayed for sport. - - Preserve boring observability around Postgres, Qdrant, Ollama, Docker, backups, watchdogs, and any private local fire-signal extensions. + - Preserve boring observability and startup discipline around Postgres, Qdrant, Ollama, Docker, backups, scheduled tasks, watchdogs, and any private local fire-signal extensions. - Continue splitting oversized runtime files where concern boundaries are already obvious, starting with the remaining bot, worker, and memory-policy monarchies. accepted_design: @@ -71,6 +71,7 @@ landed_baseline: - Postgres-backed jobs, audit events, interaction memory, and rate-limit state are live. - Qdrant-backed history and source retrieval are live. - Local and offsite backups, watchdogs, and the operations dashboard are live. + - The stack now has a dedicated logon startup task path so bot and worker can come back after a reboot without waiting for manual resurrection. - Interaction memory and behavioral profiling are live. - Source-grounding enforcement and tool-driven retrieval are live. - The operations watchdog can now load ignored local extension checks, so adjacent-service fire signals can piggyback on the owner DM path without turning the public repo into cross-project gossip. @@ -88,5 +89,5 @@ rejected_paths: reason: vibes produce plausible slop that sounds smart and lies with a straight face current_status: - summary: VoidBot now has a real project-memory spine plus four legibility-hardening passes on major runtime organs. The owner Codex lane, Discord bot lane, interaction-memory logic, and state-storage persistence layer were all split into smaller seams, and the ops spine now supports ignored local watchdog extensions for adjacent fire signals without baking private infrastructure gossip into the public repo. The stack is still dense, but it no longer depends purely on transcript memory or one-file monarchies to stay coherent. + summary: VoidBot now has a real project-memory spine plus four legibility-hardening passes on major runtime organs. The owner Codex lane, Discord bot lane, interaction-memory logic, and state-storage persistence layer were all split into smaller seams, and the ops spine now includes both ignored local watchdog extensions for adjacent fire signals and a dedicated logon startup task path for stack resurrection after reboot. The stack is still dense, but it no longer depends purely on transcript memory or one-file monarchies to stay coherent. next_action: Keep using the canonical state files for re-entry, then continue decomposing the next oversized organ with the clearest concern boundaries, likely the MCP server or local LLM provider.