diff --git a/scripts/render-voidbot-swarm-dashboard.mjs b/scripts/render-voidbot-swarm-dashboard.mjs
index 1b1608f..c79359f 100644
--- a/scripts/render-voidbot-swarm-dashboard.mjs
+++ b/scripts/render-voidbot-swarm-dashboard.mjs
@@ -1325,7 +1325,7 @@ function renderHtml(snapshot) {
const controls = snapshot.controls || {};
const latest = (controls.manualTurnRequests || [])[0];
return "
Controls
" +
- "
" +
+ "
" +
"
Manual next turn
" +
"
Latest request: " + esc(latest?.identityId || "none") + " " + esc(latest?.status || "") + "
" +
"
";
@@ -1603,7 +1603,7 @@ function renderHtml(snapshot) {
const forceButton = document.getElementById("force-turn");
if (forceSelect && selectedIdentity) forceSelect.value = selectedIdentity;
if (cadenceInput && cadenceValue) {
- cadenceInput.addEventListener("input", () => cadenceValue.textContent = "x" + Number(cadenceInput.value).toLocaleString(undefined, { maximumFractionDigits: 1 }));
+ cadenceInput.addEventListener("input", () => cadenceValue.textContent = "x" + Number(cadenceInput.value).toLocaleString(undefined, { maximumFractionDigits: 2 }));
}
if (cadenceApply && cadenceInput) {
cadenceApply.addEventListener("click", async () => {
@@ -1771,8 +1771,8 @@ async function handleControlUpdate(request, response) {
}
const body = await readRequestJson(request);
const multiplier = Number(body.cadenceMultiplier);
- if (!Number.isFinite(multiplier) || multiplier < 0.1 || multiplier > 12) {
- writeJsonResponse(response, 400, { ok: false, error: "cadenceMultiplier must be between 0.1 and 12." });
+ if (!Number.isFinite(multiplier) || multiplier < 0.01 || multiplier > 12) {
+ writeJsonResponse(response, 400, { ok: false, error: "cadenceMultiplier must be between 0.01 and 12." });
return;
}
const state = await readMutableHeartbeatState();
@@ -1827,7 +1827,7 @@ async function writeMutableHeartbeatState(state) {
function normalizeDashboardControls(value) {
const controls = value && typeof value === "object" ? value : {};
return {
- cadenceMultiplier: Math.min(12, Math.max(0.1, Number(controls.cadenceMultiplier) || 1)),
+ cadenceMultiplier: Math.min(12, Math.max(0.01, Number(controls.cadenceMultiplier) || 1)),
manualTurnRequests: Array.isArray(controls.manualTurnRequests) ? controls.manualTurnRequests : [],
updatedAt: typeof controls.updatedAt === "string" ? controls.updatedAt : undefined,
};
diff --git a/scripts/run-repo-face-heartbeats.ts b/scripts/run-repo-face-heartbeats.ts
index 3f88e49..cca28f9 100644
--- a/scripts/run-repo-face-heartbeats.ts
+++ b/scripts/run-repo-face-heartbeats.ts
@@ -272,7 +272,7 @@ async function main(): Promise {
}
state.baseRecoveryMinutes = config.repoFaceHeartbeats.baseRecoveryMinutes;
- const cadenceMultiplier = clamp(state.controls.cadenceMultiplier, 0.1, 12);
+ const cadenceMultiplier = clamp(state.controls.cadenceMultiplier, 0.01, 12);
const effectiveGlobalHeat = config.repoFaceHeartbeats.globalHeat * cadenceMultiplier;
state.globalHeat = effectiveGlobalHeat;
const completedThisTick = new Set();
@@ -1502,7 +1502,7 @@ function reconcileParticipants(
const groups = initiativeGroupsFor(spec);
const heat = heatFor(spec, groups, globalHeat, heatOverrides);
const urgency = urgencyFor(pressureByIdentity.get(spec.id));
- const effectiveSpeed = clamp(speed * heat * urgency, 0.1, 12);
+ const effectiveSpeed = clamp(speed * heat * urgency, 0.01, 12);
const defaultNextTurnAt = Number.isFinite(current?.nextTurnAt)
? current.nextTurnAt
: initiativeClock + ((baseRecoveryMinutes / count) * index);
@@ -1596,7 +1596,7 @@ function pullNextTurnTowardUrgency(
return nextTurnAt;
}
- const maxWait = Math.max(0.75, baseRecoveryMinutes / Math.max(effectiveSpeed, 0.1));
+ const maxWait = Math.max(0.75, baseRecoveryMinutes / Math.max(effectiveSpeed, 0.01));
return Math.min(nextTurnAt, round3(initiativeClock + maxWait));
}
@@ -5006,7 +5006,7 @@ function projectCharacterDescription(description: string | undefined): string |
function recoveryFor(participant: FaceHeartbeatParticipant): number {
const loadPenalty = 1 + participant.currentLoad * 0.75;
- return (participant.baseRecoveryMinutes * loadPenalty) / Math.max(participant.effectiveSpeed, 0.1);
+ return (participant.baseRecoveryMinutes * loadPenalty) / Math.max(participant.effectiveSpeed, 0.01);
}
async function readHeartbeatState(path: string): Promise {
@@ -5157,7 +5157,7 @@ function normalizeHeartbeatControls(value: unknown): FaceHeartbeatControls {
: [];
return {
- cadenceMultiplier: Number.isFinite(cadenceMultiplier) ? clamp(cadenceMultiplier, 0.1, 12) : 1,
+ cadenceMultiplier: Number.isFinite(cadenceMultiplier) ? clamp(cadenceMultiplier, 0.01, 12) : 1,
manualTurnRequests: manualTurnRequests.slice(-20),
updatedAt: typeof record.updatedAt === "string" ? record.updatedAt : undefined,
};
diff --git a/state/evidence.jsonl b/state/evidence.jsonl
index d34c75a..a9501d8 100644
--- a/state/evidence.jsonl
+++ b/state/evidence.jsonl
@@ -343,3 +343,4 @@
{"ts":"2026-05-25T07:40:27Z","type":"implementation","status":"ok","note":"Created the Discord role for the new Norn Face and updated the private repo identity registry with roleId 1508373969076748361; role-addressed mentions can now target Norn once the running bot reloads the registry."}
{"ts":"2026-05-25T12:49:01Z","type":"implementation","status":"ok","note":"Renamed the local EpiphanyGraph repo to Norn and updated the Norn Face registry/state to use E:/Projects/Norn and repo:Norn. The Norn repo builds with dotnet and the Norn viewer typecheck/build passes."}
{"ts":"2026-05-25T17:36:36Z","type":"implementation","status":"ok","note":"Revived the local VoidBot stack and repo Face swarm: npm run stack:start restored runtime-stack.json to ready with bot PID 24512 and worker PID 30640, npm run state:check passed live-service gates with only a backup warning, state/agent-swarm-paused.json is now paused=false, repo Face heartbeats and mood drift ran ok, first unpaused Void moderation turn completed ok, and Norn reconciled into the active CTB participant roster."}
+{"ts":"2026-05-25T18:53:32Z","type":"implementation","status":"ok","note":"Slowed repo Face CTB cadence by 10x in the live control surface: cadenceMultiplier is now 0.04, scheduler/dashboard/API minimum is 0.01, and effective-speed/recovery floors now preserve sub-0.1 cadence instead of silently flattening it. Typecheck and a real heartbeat pass succeeded without queuing an unprompted Face turn."}