mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
overseer fixes
This commit is contained in:
@@ -16,7 +16,6 @@ llmCommentary:
|
||||
overseerControl:
|
||||
enabled: false
|
||||
observeOnly: true
|
||||
alwaysRunModel: false
|
||||
postToolsOnlyMessages: false
|
||||
tiebreakerEnable: false
|
||||
runWhileNoPeopleOnline: false
|
||||
@@ -25,8 +24,6 @@ overseerControl:
|
||||
ollamaServer: "http://127.0.0.1:11434"
|
||||
profileImageUrl: "https://example.com/overseer.png"
|
||||
gateIntervalMs: 2000
|
||||
heartbeatMs: 30000
|
||||
postChatDelayMs: 20000
|
||||
media:
|
||||
# Base address for mediaMTX (scheme + host + optional port/path). The UI will always request
|
||||
# http://<base>/<roverId>/whep
|
||||
|
||||
@@ -3,8 +3,6 @@ const path = require('path');
|
||||
const PROMPT_PATH = path.join(__dirname, '..', '..', '..', 'prompts', 'overseer_control_system.txt');
|
||||
const DEFAULT_NAME = 'The Overseer';
|
||||
const DEFAULT_GATE_INTERVAL_MS = 2000;
|
||||
const DEFAULT_HEARTBEAT_MS = 30000;
|
||||
const DEFAULT_POST_CHAT_DELAY_MS = 20000;
|
||||
const MIN_INTERVAL_MS = 250;
|
||||
const MAX_RUN_HISTORY = 100;
|
||||
const MAX_CHAT_CONTEXT = 12;
|
||||
@@ -19,8 +17,6 @@ module.exports = {
|
||||
PROMPT_PATH,
|
||||
DEFAULT_NAME,
|
||||
DEFAULT_GATE_INTERVAL_MS,
|
||||
DEFAULT_HEARTBEAT_MS,
|
||||
DEFAULT_POST_CHAT_DELAY_MS,
|
||||
MAX_RUN_HISTORY,
|
||||
MAX_CHAT_CONTEXT,
|
||||
MAX_BOT_CONTEXT,
|
||||
|
||||
@@ -20,8 +20,6 @@ const {
|
||||
PROMPT_PATH,
|
||||
DEFAULT_NAME,
|
||||
DEFAULT_GATE_INTERVAL_MS,
|
||||
DEFAULT_HEARTBEAT_MS,
|
||||
DEFAULT_POST_CHAT_DELAY_MS,
|
||||
MAX_RUN_HISTORY,
|
||||
MAX_CHAT_CONTEXT,
|
||||
MAX_BOT_CONTEXT,
|
||||
@@ -40,9 +38,6 @@ const name = String(overseerConfig.name || DEFAULT_NAME).trim() || DEFAULT_NAME;
|
||||
const model = String(overseerConfig.model || '').trim();
|
||||
const ollamaUrl = String(overseerConfig.ollamaUrl || overseerConfig.ollamaServer || '').trim();
|
||||
const gateIntervalMs = normalizeMs(Number(overseerConfig.gateIntervalMs), DEFAULT_GATE_INTERVAL_MS);
|
||||
const heartbeatMs = normalizeMs(Number(overseerConfig.heartbeatMs), DEFAULT_HEARTBEAT_MS);
|
||||
const postChatDelayMs = normalizeMs(Number(overseerConfig.postChatDelayMs), DEFAULT_POST_CHAT_DELAY_MS);
|
||||
const alwaysRunModel = Boolean(overseerConfig.alwaysRunModel);
|
||||
const postToolsOnlyMessages = Boolean(overseerConfig.postToolsOnlyMessages);
|
||||
const tiebreakerEnable = Boolean(overseerConfig.tiebreakerEnable);
|
||||
const runWhileNoPeopleOnline = Boolean(overseerConfig.runWhileNoPeopleOnline);
|
||||
@@ -53,7 +48,6 @@ const runtime = {
|
||||
timer: null,
|
||||
inFlight: false,
|
||||
tickCount: 0,
|
||||
lastModelAt: 0,
|
||||
generationCount: 0,
|
||||
generationTotalMs: 0,
|
||||
runHistory: [],
|
||||
@@ -70,9 +64,6 @@ let status = {
|
||||
ollamaUrl,
|
||||
promptPath: PROMPT_PATH,
|
||||
gateIntervalMs,
|
||||
heartbeatMs,
|
||||
postChatDelayMs,
|
||||
alwaysRunModel,
|
||||
postToolsOnlyMessages,
|
||||
tiebreakerEnable,
|
||||
runWhileNoPeopleOnline,
|
||||
@@ -215,20 +206,6 @@ function buildRosterSummary() {
|
||||
});
|
||||
}
|
||||
|
||||
function computeTriggerReason() {
|
||||
if (alwaysRunModel) return 'loop_tick';
|
||||
const recent = getRecentMessages(1, { includeSystem: false });
|
||||
const last = recent[recent.length - 1];
|
||||
if (last && Number(last.ts || 0) < runtime.contextResetAt) return null;
|
||||
if (last && Date.now() - Number(last.ts || 0) < 5000) {
|
||||
const txt = String(last.text || '').toLowerCase();
|
||||
if (txt.includes(name.toLowerCase()) || txt.includes('overseer') || txt.includes('bot')) return 'direct_address';
|
||||
return 'chat_activity';
|
||||
}
|
||||
if (!runtime.lastModelAt || Date.now() - runtime.lastModelAt >= heartbeatMs) return 'heartbeat';
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeToolCalls(payload = null) {
|
||||
const calls = Array.isArray(payload?.message?.tool_calls) ? payload.message.tool_calls : [];
|
||||
return calls
|
||||
@@ -385,7 +362,6 @@ async function runDecision(triggerReason) {
|
||||
runtime.generationCount += 1;
|
||||
runtime.generationTotalMs += generationMs;
|
||||
const avgGenerationMs = Math.round(runtime.generationTotalMs / runtime.generationCount);
|
||||
runtime.lastModelAt = Date.now();
|
||||
|
||||
const actionResults = [];
|
||||
const requestedActions = toolCalls;
|
||||
@@ -472,21 +448,45 @@ async function runDecision(triggerReason) {
|
||||
}
|
||||
|
||||
async function tick() {
|
||||
// The scheduler is deliberately loop-only. Older versions mixed chat-triggered
|
||||
// and heartbeat-triggered paths into this same timer, which made it possible
|
||||
// for one slow model generation to be followed by another run that was still
|
||||
// reasoning over the same chat message. In the loop model, every completed
|
||||
// timer means exactly one model request, and the next timer is not installed
|
||||
// until the current request has completely finished.
|
||||
const startedAt = Date.now();
|
||||
|
||||
// setTimeout handles are one-shot. Clearing the reference at the beginning of
|
||||
// the callback makes scheduler ownership obvious: if any gate reevaluation
|
||||
// happens while this tick is running, startScheduler can see that work is
|
||||
// already in-flight and will not create a competing timer.
|
||||
runtime.timer = null;
|
||||
|
||||
if (runtime.inFlight) {
|
||||
// This is a defensive guard for unusual event-loop races or manual calls.
|
||||
// Sending the same model input twice is worse than skipping one interval,
|
||||
// so a re-entrant tick is ignored and the active tick remains responsible
|
||||
// for scheduling the next pass.
|
||||
updateStatus({
|
||||
phase: 'idle',
|
||||
lastOutcome: 'skipped',
|
||||
lastReason: 'loop tick skipped while model request is in flight',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
runtime.tickCount += 1;
|
||||
runtime.inFlight = true;
|
||||
updateStatus({ inFlight: true, tickCount: runtime.tickCount, lastTickAt: Date.now(), phase: 'gate_check' });
|
||||
updateStatus({
|
||||
inFlight: true,
|
||||
tickCount: runtime.tickCount,
|
||||
lastTickAt: startedAt,
|
||||
phase: 'loop_tick',
|
||||
lastTriggerReason: 'loop_tick',
|
||||
});
|
||||
|
||||
let nextDelayMs = gateIntervalMs;
|
||||
try {
|
||||
const triggerReason = computeTriggerReason();
|
||||
if (!triggerReason) {
|
||||
updateStatus({ phase: 'idle', lastOutcome: 'skipped', lastReason: 'gate not triggered' });
|
||||
} else {
|
||||
const runResult = await runDecision(triggerReason);
|
||||
if (runResult?.postedChat) {
|
||||
nextDelayMs = postChatDelayMs;
|
||||
}
|
||||
}
|
||||
await runDecision('loop_tick');
|
||||
} catch (err) {
|
||||
const failure = buildFailureInfo(err);
|
||||
updateStatus({
|
||||
@@ -500,8 +500,13 @@ async function tick() {
|
||||
} finally {
|
||||
runtime.inFlight = false;
|
||||
if (status.running) {
|
||||
updateStatus({ inFlight: false, currentRunId: null, phase: 'idle', nextRunAt: Date.now() + nextDelayMs });
|
||||
runtime.timer = setTimeout(tick, nextDelayMs);
|
||||
const nextRunAt = Date.now() + gateIntervalMs;
|
||||
updateStatus({ inFlight: false, currentRunId: null, phase: 'idle', nextRunAt });
|
||||
|
||||
// The next timer is installed after all async work completes. This keeps
|
||||
// Ollama calls strictly serialized even when a generation takes longer
|
||||
// than gateIntervalMs.
|
||||
runtime.timer = setTimeout(tick, gateIntervalMs);
|
||||
} else {
|
||||
updateStatus({ inFlight: false, currentRunId: null, nextRunAt: null });
|
||||
}
|
||||
@@ -521,7 +526,6 @@ function clearHistory(reason = 'admin requested clear history', options = {}) {
|
||||
runtime.liveToolCalls = [];
|
||||
runtime.generationCount = 0;
|
||||
runtime.generationTotalMs = 0;
|
||||
runtime.lastModelAt = 0;
|
||||
if (resetPersistentMemory) {
|
||||
runtime.memoryStore = saveMemory(createDefaultMemory());
|
||||
}
|
||||
@@ -567,14 +571,17 @@ function stopScheduler(reason = 'paused') {
|
||||
}
|
||||
|
||||
function startScheduler(reason = null) {
|
||||
if (runtime.timer) return;
|
||||
updateStatus({ running: true, phase: 'idle', lastReason: reason });
|
||||
// Gate changes can happen while a loop iteration is still waiting on Ollama.
|
||||
// In that case we mark the service as running again, but let the active tick's
|
||||
// finally block install the next timer. That preserves a single owner for
|
||||
// scheduling and prevents overlapping model requests.
|
||||
updateStatus({ running: true, phase: runtime.inFlight ? status.phase : 'idle', lastReason: reason });
|
||||
if (runtime.timer || runtime.inFlight) return;
|
||||
runtime.timer = setTimeout(tick, gateIntervalMs);
|
||||
}
|
||||
|
||||
function evaluateSchedulerGate(reason = 'gate reevaluated') {
|
||||
const voteStatus = buildVoteStatus();
|
||||
const runningAllowed = Boolean(enabled && voteStatus.gatePassed && getMode() !== MODES.LOCKDOWN);
|
||||
updateStatus({ voteStatus });
|
||||
if (!enabled) {
|
||||
stopScheduler('overseerControl.enabled is false');
|
||||
@@ -640,7 +647,7 @@ if (!enabled) {
|
||||
logger.info('overseerControl paused on startup due to lockdown mode');
|
||||
} else {
|
||||
evaluateSchedulerGate(observeOnly ? 'observe-only mode' : null);
|
||||
logger.info('overseerControl enabled', { model, ollamaUrl, gateIntervalMs, heartbeatMs, observeOnly });
|
||||
logger.info('overseerControl enabled', { model, ollamaUrl, gateIntervalMs, observeOnly });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,8 +21,6 @@ function buildAdminState(status, runHistory) {
|
||||
model: status.model,
|
||||
ollamaUrl: status.ollamaUrl,
|
||||
gateIntervalMs: status.gateIntervalMs,
|
||||
heartbeatMs: status.heartbeatMs,
|
||||
alwaysRunModel: status.alwaysRunModel,
|
||||
observeOnly: status.observeOnly,
|
||||
promptPath: status.promptPath,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user