From 40cbdd075a5b313778a58372d50ff7beedac02c8 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 24 May 2026 22:26:01 +0100 Subject: [PATCH] Slow Face idle cooling into nap mode --- .env.example | 4 ++++ packages/config/src/index.ts | 7 +++++-- scripts/run-repo-face-heartbeats.ts | 30 ++++++++++++++++++++++++++--- state/evidence.jsonl | 1 + 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/.env.example b/.env.example index fb7824f..99a3d77 100644 --- a/.env.example +++ b/.env.example @@ -73,6 +73,10 @@ REPO_FACE_HEARTBEAT_TASK_NAME=VoidBot Repo Face Heartbeats REPO_FACE_HEARTBEAT_INTERVAL_MINUTES=15 REPO_FACE_HEARTBEAT_MAX_JOBS_PER_TICK=2 REPO_FACE_HEARTBEAT_DEFAULT_CHANNEL_ID= +REPO_FACE_HEARTBEAT_IDLE_COOLING_ENABLED=true +REPO_FACE_HEARTBEAT_IDLE_AFTER_MINUTES=180 +REPO_FACE_HEARTBEAT_IDLE_RECOVERY_MINUTES=120 +REPO_FACE_HEARTBEAT_IDLE_NAP_AFTER_MINUTES=360 REPO_FACE_HEARTBEAT_CODEX_MODEL=gpt-5.3-codex-spark REPO_FACE_HEARTBEAT_CODEX_MODELS=gpt-5.3-codex-spark,gpt-5.4-mini REPO_FACE_TURN_CODEX_MODEL=gpt-5.4 diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index 8ffe5dd..6b0ae46 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -116,8 +116,9 @@ const envSchema = z.object({ REPO_FACE_HEARTBEAT_BASE_RECOVERY_MINUTES: z.coerce.number().positive().default(10), REPO_FACE_HEARTBEAT_GLOBAL_HEAT: z.coerce.number().positive().default(1), REPO_FACE_HEARTBEAT_IDLE_COOLING_ENABLED: booleanFromEnv.default(true), - REPO_FACE_HEARTBEAT_IDLE_AFTER_MINUTES: z.coerce.number().positive().default(30), - REPO_FACE_HEARTBEAT_IDLE_RECOVERY_MINUTES: z.coerce.number().positive().default(30), + REPO_FACE_HEARTBEAT_IDLE_AFTER_MINUTES: z.coerce.number().positive().default(180), + REPO_FACE_HEARTBEAT_IDLE_RECOVERY_MINUTES: z.coerce.number().positive().default(120), + REPO_FACE_HEARTBEAT_IDLE_NAP_AFTER_MINUTES: z.coerce.number().positive().default(360), REPO_FACE_HEARTBEAT_SPEED_OVERRIDES: z.string().default(""), REPO_FACE_HEARTBEAT_HEAT_OVERRIDES: z.string().default(""), REPO_FACE_HEARTBEAT_CODEX_MODEL: z.string().min(1).default("gpt-5.3-codex-spark"), @@ -181,6 +182,7 @@ export interface AppConfig { enabled: boolean; idleAfterMinutes: number; recoveryMinutes: number; + napAfterMinutes: number; }; speedOverrides: Record; heatOverrides: Record; @@ -412,6 +414,7 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig { enabled: parsed.REPO_FACE_HEARTBEAT_IDLE_COOLING_ENABLED, idleAfterMinutes: parsed.REPO_FACE_HEARTBEAT_IDLE_AFTER_MINUTES, recoveryMinutes: parsed.REPO_FACE_HEARTBEAT_IDLE_RECOVERY_MINUTES, + napAfterMinutes: parsed.REPO_FACE_HEARTBEAT_IDLE_NAP_AFTER_MINUTES, }, speedOverrides: parseNumericMap(parsed.REPO_FACE_HEARTBEAT_SPEED_OVERRIDES), heatOverrides: parseNumericMap(parsed.REPO_FACE_HEARTBEAT_HEAT_OVERRIDES), diff --git a/scripts/run-repo-face-heartbeats.ts b/scripts/run-repo-face-heartbeats.ts index 7937930..abc30ba 100644 --- a/scripts/run-repo-face-heartbeats.ts +++ b/scripts/run-repo-face-heartbeats.ts @@ -93,6 +93,8 @@ interface IdleCoolingSnapshot { idleForMinutes?: number; idleAfterMinutes: number; recoveryMinutes: number; + napAfterMinutes: number; + napModeActive?: boolean; lastUnpromptedTurnQueuedAt?: string; nextUnpromptedTurnAllowedAt?: string; } @@ -824,6 +826,9 @@ async function maybeStartRepoFaceMemoryMaintenance(input: { if (isRecentRunningStatus(currentStatus, 25)) { return { started: false, reason: "maintenance_already_running", statusPath: paths.statusPath }; } + if (input.projectedRest.isNapping === true && isStatusCompletedRecently(currentStatus, 60)) { + return { started: false, reason: "nap_maintenance_recently_completed", statusPath: paths.statusPath }; + } if (isStatusCompletedAfter(currentStatus, newestTimestampMs(shortTerm))) { return { started: false, reason: "maintenance_already_completed_for_sources", statusPath: paths.statusPath }; } @@ -913,6 +918,14 @@ function isStatusCompletedAfter(status: Record | undefined, sou return Number.isFinite(finishedAtMs) && finishedAtMs >= sourceTimestampMs; } +function isStatusCompletedRecently(status: Record | undefined, maxAgeMinutes: number): boolean { + if (status?.status !== "ok") { + return false; + } + const finishedAtMs = typeof status.finishedAt === "string" ? Date.parse(status.finishedAt) : NaN; + return Number.isFinite(finishedAtMs) && Date.now() - finishedAtMs < maxAgeMinutes * 60_000; +} + function newestTimestampMs(entries: Array<{ updatedAt: string }>): number { return Math.max(...entries.map((entry) => Date.parse(entry.updatedAt)).filter(Number.isFinite)); } @@ -1560,7 +1573,8 @@ function selectReadyParticipants( !( participant.participantKind === "repo_face" && pendingMentionCount === 0 && - restState?.isNapping === true + restState?.isNapping === true && + idleCooling.napModeActive !== true ) ); }); @@ -1597,10 +1611,16 @@ function selectReadyParticipants( const mentioned = ready.filter((participant) => (pendingMentionCounts.get(participant.identityId) ?? 0) > 0); const unprompted = ready.filter((participant) => (pendingMentionCounts.get(participant.identityId) ?? 0) === 0); + const napRuns = idleCooling.napModeActive === true + ? unprompted.filter((participant) => restStates.get(participant.identityId)?.isNapping === true) + : []; + const awakeRuns = unprompted.filter((participant) => restStates.get(participant.identityId)?.isNapping !== true); const coolingAllowsUnpromptedTurn = !idleCooling.nextUnpromptedTurnAllowedAt || Date.parse(idleCooling.nextUnpromptedTurnAllowedAt) <= Date.now(); - const cooled = coolingAllowsUnpromptedTurn && mentioned.length < maxJobs ? unprompted.slice(0, 1) : []; + const cooled = coolingAllowsUnpromptedTurn && mentioned.length < maxJobs + ? (napRuns[0] ? napRuns : awakeRuns).slice(0, 1) + : []; return [...mentioned, ...cooled].slice(0, maxJobs); } @@ -1690,6 +1710,7 @@ async function readIdleCoolingSnapshot(input: { checkedChannelIds, idleAfterMinutes: policy.idleAfterMinutes, recoveryMinutes: policy.recoveryMinutes, + napAfterMinutes: policy.napAfterMinutes, lastUnpromptedTurnQueuedAt: newestUnpromptedTurnQueuedAt(input.state), }; @@ -1737,15 +1758,18 @@ async function readIdleCoolingSnapshot(input: { return { ...base, active: true, + napModeActive: true, reason: fetchErrors.length > 0 ? "activity_fetch_failed_or_no_human_messages" : "no_recent_human_messages", nextUnpromptedTurnAllowedAt, }; } const idleForMinutes = Math.max(0, (input.now.getTime() - Date.parse(newestHumanActivityAt)) / 60_000); + const active = idleForMinutes >= policy.idleAfterMinutes; return { ...base, - active: idleForMinutes >= policy.idleAfterMinutes, + active, + napModeActive: active && idleForMinutes >= policy.napAfterMinutes, reason: fetchErrors.length > 0 ? "partial_activity_fetch_failure" : undefined, lastHumanActivityAt: newestHumanActivityAt, idleForMinutes: round3(idleForMinutes), diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 35bef82..9bd47d4 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -322,3 +322,4 @@ {"ts":"2026-05-24T12:09:30Z","type":"implementation","status":"ok","note":"CultCacheTS state writes now retry transient Windows EPERM/EBUSY/EACCES replace failures before falling back to copy-over-write. This protects repo Face .cc operations when the live bot, scanner, or another reader briefly pins the target state file during temp-file replacement."} {"ts":"2026-05-24T18:36:00Z","type":"implementation","status":"ok","note":"Repo Face CTB urgency now derives from typed speaking pressure; Kiko's StreamPixels state carries a June 10 2026 delivery pressure for Metacrat's sister's project."} {"ts":"2026-05-24T19:38:23Z","type":"implementation","status":"ok","note":"Repo Face affect now has socialBiases for neuroticism, threat sensitivity, hostile attribution, reassurance need, grievance retention, status vigilance, and trust baseline. Active Faces plus Void were seeded with archetypal bias baselines; Nibu's projection smoke shows the biases reaching child prompt context."} +{"ts":"2026-05-24T21:25:43Z","type":"implementation","status":"ok","note":"Repo Face idle cooling now decays over hours by default: cooling after 180 minutes, nap mode after 360 minutes, cooled unprompted turns every 120 minutes; nap mode can select napping Faces for sparse dream turns and nap memory maintenance has a 60-minute completion cooldown."}