Slow Face idle cooling into nap mode
This commit is contained in:
parent
3424486767
commit
40cbdd075a
4 changed files with 37 additions and 5 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<string, number>;
|
||||
heatOverrides: Record<string, number>;
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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<string, unknown> | undefined, sou
|
|||
return Number.isFinite(finishedAtMs) && finishedAtMs >= sourceTimestampMs;
|
||||
}
|
||||
|
||||
function isStatusCompletedRecently(status: Record<string, unknown> | 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),
|
||||
|
|
|
|||
|
|
@ -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."}
|
||||
|
|
|
|||
Loading…
Reference in a new issue