mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
overseer v2 1
This commit is contained in:
@@ -13,6 +13,14 @@ llmCommentary:
|
|||||||
model: "qwen2.5:7b-instruct"
|
model: "qwen2.5:7b-instruct"
|
||||||
ollamaServer: "http://127.0.0.1:11434"
|
ollamaServer: "http://127.0.0.1:11434"
|
||||||
frequency: 120000
|
frequency: 120000
|
||||||
|
overseerControl:
|
||||||
|
enabled: false
|
||||||
|
observeOnly: true
|
||||||
|
name: "The Overseer"
|
||||||
|
model: "qwen2.5:7b-instruct"
|
||||||
|
ollamaServer: "http://127.0.0.1:11434"
|
||||||
|
gateIntervalMs: 2000
|
||||||
|
heartbeatMs: 30000
|
||||||
media:
|
media:
|
||||||
# Base address for mediaMTX (scheme + host + optional port/path). The UI will always request
|
# Base address for mediaMTX (scheme + host + optional port/path). The UI will always request
|
||||||
# http://<base>/<roverId>/whep
|
# http://<base>/<roverId>/whep
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ require('./src/services/verificationService');
|
|||||||
require('./src/services/privateRoverAccessRequestService');
|
require('./src/services/privateRoverAccessRequestService');
|
||||||
require('./src/services/chatService');
|
require('./src/services/chatService');
|
||||||
require('./src/services/llmCommentaryService');
|
require('./src/services/llmCommentaryService');
|
||||||
|
require('./src/services/overseerControlService');
|
||||||
require('./src/services/globalObjectiveService');
|
require('./src/services/globalObjectiveService');
|
||||||
require('./src/services/serverControlService');
|
require('./src/services/serverControlService');
|
||||||
require('./src/services/videoSessions');
|
require('./src/services/videoSessions');
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
You are <NAME>, a control-first room AI for a live rover room.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- You may choose SKIP, CHAT, ACTION, or ACTION+CHAT.
|
||||||
|
- Respect tool availability and blocked tool reasons.
|
||||||
|
- Prefer restraint over spam.
|
||||||
|
- If directly addressed, avoid ignoring users.
|
||||||
|
- Keep lines concise and in-character.
|
||||||
|
|
||||||
|
Output contract for this phase:
|
||||||
|
- Return exactly one line.
|
||||||
|
- Return one of: SKIP, CHAT, ACTION, ACTION+CHAT.
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -10,8 +10,8 @@
|
|||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
|
||||||
<title>Roomba Rover</title>
|
<title>Multi Roomba Rover</title>
|
||||||
<script type="module" crossorigin src="/assets/index-BVMv97rc.js"></script>
|
<script type="module" crossorigin src="/assets/index-Ctsy7ecD.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-CGvUXLso.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-CGvUXLso.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ function buildMessage(socket, text, meta = {}) {
|
|||||||
text,
|
text,
|
||||||
tts: meta.tts || null,
|
tts: meta.tts || null,
|
||||||
system: Boolean(meta.system),
|
system: Boolean(meta.system),
|
||||||
|
bot: Boolean(meta.bot),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ function sendSystemMessage(text) {
|
|||||||
role: 'user',
|
role: 'user',
|
||||||
fromDiscord: false,
|
fromDiscord: false,
|
||||||
system: true,
|
system: true,
|
||||||
|
bot: true,
|
||||||
});
|
});
|
||||||
broadcastMessage(message);
|
broadcastMessage(message);
|
||||||
return message;
|
return message;
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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 MIN_INTERVAL_MS = 250;
|
||||||
|
const MAX_RUN_HISTORY = 100;
|
||||||
|
const MAX_CHAT_CONTEXT = 12;
|
||||||
|
const MAX_BOT_CONTEXT = 2;
|
||||||
|
|
||||||
|
function normalizeMs(value, fallback) {
|
||||||
|
if (!Number.isFinite(value)) return fallback;
|
||||||
|
return Math.max(MIN_INTERVAL_MS, Math.floor(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
PROMPT_PATH,
|
||||||
|
DEFAULT_NAME,
|
||||||
|
DEFAULT_GATE_INTERVAL_MS,
|
||||||
|
DEFAULT_HEARTBEAT_MS,
|
||||||
|
MAX_RUN_HISTORY,
|
||||||
|
MAX_CHAT_CONTEXT,
|
||||||
|
MAX_BOT_CONTEXT,
|
||||||
|
normalizeMs,
|
||||||
|
};
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
const { evaluateTools } = require('./tools');
|
||||||
|
|
||||||
|
function toStateUpdate({ mode, homeAssistantState, neatoState, liftState, roster, triggerReason }) {
|
||||||
|
const lines = [];
|
||||||
|
lines.push(`trigger: ${triggerReason || 'heartbeat'}`);
|
||||||
|
lines.push(`mode: ${mode || 'unknown'}`);
|
||||||
|
lines.push(`home_assistant_connected: ${homeAssistantState?.connected ? 'yes' : 'no'}`);
|
||||||
|
lines.push(`lights_locked_on: ${homeAssistantState?.lightPolicy?.lockedOn ? 'yes' : 'no'}`);
|
||||||
|
lines.push(`lift: ${liftState?.connected ? 'connected' : 'offline'} busy=${liftState?.busy ? 'yes' : 'no'}`);
|
||||||
|
lines.push(`neato: ${neatoState?.connected ? 'connected' : 'offline'} state=${neatoState?.telemetry?.robotState || 'unknown'}`);
|
||||||
|
const roverLines = (Array.isArray(roster) ? roster : []).slice(0, 6).map((rover) => {
|
||||||
|
const roverId = rover?.id || 'unknown';
|
||||||
|
const driver = rover?.driverNickname || 'none';
|
||||||
|
const status = rover?.statusTag || 'unknown';
|
||||||
|
return `- ${roverId} status=${status} driver=${driver}`;
|
||||||
|
});
|
||||||
|
if (roverLines.length) {
|
||||||
|
lines.push('rovers:');
|
||||||
|
lines.push(...roverLines);
|
||||||
|
}
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildToolState({ mode, homeAssistantState, neatoState, liftState }) {
|
||||||
|
return evaluateTools({ mode, homeAssistantState, neatoState, liftState });
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildConversation({ recentMessages, name }) {
|
||||||
|
const rows = [];
|
||||||
|
(recentMessages || []).forEach((entry) => {
|
||||||
|
const text = String(entry?.text || '').trim();
|
||||||
|
if (!text) return;
|
||||||
|
const who = entry?.system ? (entry?.nickname || name || 'Overseer') : (entry?.nickname || 'user');
|
||||||
|
rows.push(`${who}: ${text}`);
|
||||||
|
});
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildModelMessages({ systemPrompt, stateUpdate, transcriptRows, availableTools, blockedTools }) {
|
||||||
|
const messages = [];
|
||||||
|
messages.push({ role: 'system', content: systemPrompt });
|
||||||
|
messages.push({ role: 'user', content: `STATE_UPDATE\n${stateUpdate}` });
|
||||||
|
transcriptRows.forEach((row) => messages.push({ role: 'user', content: row }));
|
||||||
|
messages.push({
|
||||||
|
role: 'user',
|
||||||
|
content: `available_tools:\n${availableTools.map((tool) => `- ${tool}`).join('\n') || '- none'}`,
|
||||||
|
});
|
||||||
|
messages.push({
|
||||||
|
role: 'user',
|
||||||
|
content: `blocked_tools:\n${blockedTools.map((entry) => `- ${entry.tool} reason=${entry.reason}`).join('\n') || '- none'}`,
|
||||||
|
});
|
||||||
|
messages.push({
|
||||||
|
role: 'user',
|
||||||
|
content: 'Respond with one line: SKIP, CHAT, ACTION, or ACTION+CHAT.',
|
||||||
|
});
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
toStateUpdate,
|
||||||
|
buildToolState,
|
||||||
|
buildConversation,
|
||||||
|
buildModelMessages,
|
||||||
|
};
|
||||||
@@ -0,0 +1,343 @@
|
|||||||
|
const fsp = require('fs/promises');
|
||||||
|
const { Ollama } = require('ollama');
|
||||||
|
const io = require('../../globals/io');
|
||||||
|
const logger = require('../../globals/logger').child('overseerControl');
|
||||||
|
const { loadConfig } = require('../../helpers/configLoader');
|
||||||
|
const { getRole, roleEvents } = require('../roleService');
|
||||||
|
const { getMode } = require('../modeManager');
|
||||||
|
const { getState: getHomeAssistantState, homeAssistantEvents } = require('../homeAssistantService');
|
||||||
|
const { getState: getNeatoState, neatoEvents } = require('../neatoService');
|
||||||
|
const { getState: getLiftState, liftEvents } = require('../liftService');
|
||||||
|
const roverManager = require('../roverManager');
|
||||||
|
const { getRecentMessages } = require('../chatService');
|
||||||
|
const {
|
||||||
|
PROMPT_PATH,
|
||||||
|
DEFAULT_NAME,
|
||||||
|
DEFAULT_GATE_INTERVAL_MS,
|
||||||
|
DEFAULT_HEARTBEAT_MS,
|
||||||
|
MAX_RUN_HISTORY,
|
||||||
|
MAX_CHAT_CONTEXT,
|
||||||
|
MAX_BOT_CONTEXT,
|
||||||
|
normalizeMs,
|
||||||
|
} = require('./constants');
|
||||||
|
const {
|
||||||
|
isAdminRole,
|
||||||
|
buildAdminState,
|
||||||
|
normalizeDecision,
|
||||||
|
buildFailureInfo,
|
||||||
|
} = require('./runtimeHelpers');
|
||||||
|
const {
|
||||||
|
toStateUpdate,
|
||||||
|
buildToolState,
|
||||||
|
buildConversation,
|
||||||
|
buildModelMessages,
|
||||||
|
} = require('./contextBuilder');
|
||||||
|
|
||||||
|
const config = loadConfig();
|
||||||
|
const overseerConfig = config.overseerControl || {};
|
||||||
|
const enabled = Boolean(overseerConfig.enabled);
|
||||||
|
const observeOnly = overseerConfig.observeOnly !== false;
|
||||||
|
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 ollamaClient = ollamaUrl ? new Ollama({ host: ollamaUrl }) : null;
|
||||||
|
|
||||||
|
const runtime = {
|
||||||
|
timer: null,
|
||||||
|
inFlight: false,
|
||||||
|
running: false,
|
||||||
|
tickCount: 0,
|
||||||
|
lastModelAt: 0,
|
||||||
|
generationCount: 0,
|
||||||
|
generationTotalMs: 0,
|
||||||
|
runHistory: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
let status = {
|
||||||
|
enabled,
|
||||||
|
observeOnly,
|
||||||
|
name,
|
||||||
|
model,
|
||||||
|
ollamaUrl,
|
||||||
|
promptPath: PROMPT_PATH,
|
||||||
|
gateIntervalMs,
|
||||||
|
heartbeatMs,
|
||||||
|
running: false,
|
||||||
|
inFlight: false,
|
||||||
|
phase: 'idle',
|
||||||
|
phaseAt: Date.now(),
|
||||||
|
tickCount: 0,
|
||||||
|
currentRunId: null,
|
||||||
|
nextRunAt: null,
|
||||||
|
lastTickAt: null,
|
||||||
|
lastTriggerReason: null,
|
||||||
|
lastSystemPrompt: null,
|
||||||
|
lastStateUpdate: null,
|
||||||
|
lastTranscript: null,
|
||||||
|
lastAvailableTools: null,
|
||||||
|
lastBlockedTools: null,
|
||||||
|
lastModelMessages: null,
|
||||||
|
lastModelInputAt: null,
|
||||||
|
lastModelOutputAt: null,
|
||||||
|
lastModelRawOutput: null,
|
||||||
|
lastDecision: null,
|
||||||
|
lastOutcome: null,
|
||||||
|
lastReason: null,
|
||||||
|
lastError: null,
|
||||||
|
lastErrorDetails: null,
|
||||||
|
lastFailedAt: null,
|
||||||
|
lastGenerationMs: null,
|
||||||
|
avgGenerationMs: null,
|
||||||
|
generationCount: 0,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
function updateStatus(patch = {}) {
|
||||||
|
status = {
|
||||||
|
...status,
|
||||||
|
...patch,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
};
|
||||||
|
const payload = buildAdminState(status, runtime.runHistory);
|
||||||
|
io.sockets.sockets.forEach((socket) => {
|
||||||
|
if (!isAdminRole(getRole(socket))) return;
|
||||||
|
socket.emit('overseer:state', payload);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushRun(run = {}) {
|
||||||
|
runtime.runHistory = [...runtime.runHistory.slice(-(MAX_RUN_HISTORY - 1)), run];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readPrompt() {
|
||||||
|
const raw = await fsp.readFile(PROMPT_PATH, 'utf8');
|
||||||
|
const prompt = String(raw || '').replace(/<NAME>/g, name).trim();
|
||||||
|
if (!prompt) throw new Error(`Prompt file empty: ${PROMPT_PATH}`);
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRosterSummary() {
|
||||||
|
return roverManager.getRoster().map((rover) => ({
|
||||||
|
id: rover?.id || 'unknown',
|
||||||
|
statusTag: rover?.statusTag || 'unknown',
|
||||||
|
driverNickname: rover?.driverNickname || null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeTriggerReason() {
|
||||||
|
const recent = getRecentMessages(1, { includeSystem: false });
|
||||||
|
const last = recent[recent.length - 1];
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runDecision(triggerReason) {
|
||||||
|
const runId = runtime.tickCount;
|
||||||
|
updateStatus({
|
||||||
|
phase: 'context_build',
|
||||||
|
currentRunId: runId,
|
||||||
|
lastTriggerReason: triggerReason,
|
||||||
|
lastError: null,
|
||||||
|
lastErrorDetails: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mode = getMode();
|
||||||
|
const homeAssistantState = getHomeAssistantState();
|
||||||
|
const neatoState = getNeatoState();
|
||||||
|
const liftState = getLiftState();
|
||||||
|
const roster = buildRosterSummary();
|
||||||
|
|
||||||
|
const stateUpdate = toStateUpdate({ mode, homeAssistantState, neatoState, liftState, roster, triggerReason });
|
||||||
|
const toolState = buildToolState({ mode, homeAssistantState, neatoState, liftState });
|
||||||
|
|
||||||
|
const human = getRecentMessages(MAX_CHAT_CONTEXT, { includeSystem: false });
|
||||||
|
const bots = getRecentMessages(100, { includeSystem: true })
|
||||||
|
.filter((entry) => entry?.system)
|
||||||
|
.slice(-MAX_BOT_CONTEXT);
|
||||||
|
const transcriptRows = buildConversation({ recentMessages: [...human, ...bots].slice(-(MAX_CHAT_CONTEXT + MAX_BOT_CONTEXT)), name });
|
||||||
|
|
||||||
|
const systemPrompt = await readPrompt();
|
||||||
|
const modelMessages = buildModelMessages({
|
||||||
|
systemPrompt,
|
||||||
|
stateUpdate,
|
||||||
|
transcriptRows,
|
||||||
|
availableTools: toolState.available,
|
||||||
|
blockedTools: toolState.blocked,
|
||||||
|
});
|
||||||
|
|
||||||
|
updateStatus({
|
||||||
|
phase: 'awaiting_model',
|
||||||
|
lastSystemPrompt: systemPrompt,
|
||||||
|
lastStateUpdate: stateUpdate,
|
||||||
|
lastTranscript: transcriptRows,
|
||||||
|
lastAvailableTools: toolState.available,
|
||||||
|
lastBlockedTools: toolState.blocked,
|
||||||
|
lastModelMessages: modelMessages,
|
||||||
|
lastModelInputAt: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let decision = 'SKIP';
|
||||||
|
let rawOutput = '';
|
||||||
|
const generationStart = Date.now();
|
||||||
|
if (ollamaClient && model) {
|
||||||
|
const payload = await ollamaClient.chat({
|
||||||
|
model,
|
||||||
|
stream: false,
|
||||||
|
keep_alive: -1,
|
||||||
|
options: {
|
||||||
|
temperature: 0.4,
|
||||||
|
top_p: 0.9,
|
||||||
|
},
|
||||||
|
messages: modelMessages,
|
||||||
|
});
|
||||||
|
const parsed = normalizeDecision(payload?.message?.content || '');
|
||||||
|
rawOutput = parsed.raw;
|
||||||
|
decision = parsed.decision;
|
||||||
|
}
|
||||||
|
|
||||||
|
const generationMs = Math.max(0, Date.now() - generationStart);
|
||||||
|
runtime.generationCount += 1;
|
||||||
|
runtime.generationTotalMs += generationMs;
|
||||||
|
const avgGenerationMs = Math.round(runtime.generationTotalMs / runtime.generationCount);
|
||||||
|
|
||||||
|
runtime.lastModelAt = Date.now();
|
||||||
|
const outcome = observeOnly ? 'observed' : 'pending_execution';
|
||||||
|
|
||||||
|
updateStatus({
|
||||||
|
phase: 'decision_recorded',
|
||||||
|
lastModelOutputAt: Date.now(),
|
||||||
|
lastModelRawOutput: rawOutput,
|
||||||
|
lastDecision: decision,
|
||||||
|
lastOutcome: outcome,
|
||||||
|
lastReason: observeOnly ? 'observe-only mode' : null,
|
||||||
|
lastGenerationMs: generationMs,
|
||||||
|
avgGenerationMs,
|
||||||
|
generationCount: runtime.generationCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
pushRun({
|
||||||
|
runId,
|
||||||
|
at: Date.now(),
|
||||||
|
triggerReason,
|
||||||
|
decision,
|
||||||
|
outcome,
|
||||||
|
observeOnly,
|
||||||
|
generationMs,
|
||||||
|
blockedTools: toolState.blocked,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tick() {
|
||||||
|
runtime.tickCount += 1;
|
||||||
|
runtime.inFlight = true;
|
||||||
|
updateStatus({
|
||||||
|
inFlight: true,
|
||||||
|
tickCount: runtime.tickCount,
|
||||||
|
lastTickAt: Date.now(),
|
||||||
|
phase: 'gate_check',
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const triggerReason = computeTriggerReason();
|
||||||
|
if (!triggerReason) {
|
||||||
|
updateStatus({
|
||||||
|
phase: 'idle',
|
||||||
|
lastOutcome: 'skipped',
|
||||||
|
lastReason: 'gate not triggered',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await runDecision(triggerReason);
|
||||||
|
} catch (err) {
|
||||||
|
const failure = buildFailureInfo(err);
|
||||||
|
updateStatus({
|
||||||
|
phase: 'failed',
|
||||||
|
lastError: failure.message,
|
||||||
|
lastErrorDetails: failure.details,
|
||||||
|
lastFailedAt: Date.now(),
|
||||||
|
lastOutcome: 'failed',
|
||||||
|
lastReason: 'exception',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
runtime.inFlight = false;
|
||||||
|
updateStatus({
|
||||||
|
inFlight: false,
|
||||||
|
currentRunId: null,
|
||||||
|
phase: 'idle',
|
||||||
|
nextRunAt: Date.now() + gateIntervalMs,
|
||||||
|
});
|
||||||
|
runtime.timer = setTimeout(tick, gateIntervalMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitStateToSocket(socket) {
|
||||||
|
if (!socket || !isAdminRole(getRole(socket))) return;
|
||||||
|
socket.emit('overseer:state', buildAdminState(status, runtime.runHistory));
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearHistory() {
|
||||||
|
runtime.runHistory = [];
|
||||||
|
runtime.generationCount = 0;
|
||||||
|
runtime.generationTotalMs = 0;
|
||||||
|
updateStatus({
|
||||||
|
lastReason: 'admin requested clear history',
|
||||||
|
lastOutcome: 'cleared',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
io.on('connection', (socket) => {
|
||||||
|
emitStateToSocket(socket);
|
||||||
|
socket.on('overseer:control', ({ controls } = {}, cb = () => {}) => {
|
||||||
|
if (!isAdminRole(getRole(socket))) {
|
||||||
|
cb({ error: 'Not authorized' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const action = controls?.action || null;
|
||||||
|
if (action === 'clearHistory') {
|
||||||
|
clearHistory();
|
||||||
|
cb({ success: true, state: buildAdminState(status, runtime.runHistory) });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cb({ error: 'Unknown overseer control action' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
roleEvents.on('change', ({ socket }) => {
|
||||||
|
emitStateToSocket(socket);
|
||||||
|
});
|
||||||
|
|
||||||
|
homeAssistantEvents.on('update', () => {
|
||||||
|
updateStatus({ phase: status.phase });
|
||||||
|
});
|
||||||
|
neatoEvents.on('update', () => {
|
||||||
|
updateStatus({ phase: status.phase });
|
||||||
|
});
|
||||||
|
liftEvents.on('update', () => {
|
||||||
|
updateStatus({ phase: status.phase });
|
||||||
|
});
|
||||||
|
roverManager.managerEvents.on('rover', () => {
|
||||||
|
updateStatus({ phase: status.phase });
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!enabled) {
|
||||||
|
logger.info('overseerControl disabled');
|
||||||
|
updateStatus({ running: false, lastReason: 'overseerControl.enabled is false' });
|
||||||
|
} else {
|
||||||
|
runtime.running = true;
|
||||||
|
updateStatus({ running: true, lastReason: observeOnly ? 'observe-only mode' : null });
|
||||||
|
runtime.timer = setTimeout(tick, gateIntervalMs);
|
||||||
|
logger.info('overseerControl enabled', { model, ollamaUrl, gateIntervalMs, heartbeatMs, observeOnly });
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {};
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
function isAdminRole(role) {
|
||||||
|
return role === 'admin' || role === 'lockdown' || role === 'lockdown-admin';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAdminState(status, runHistory) {
|
||||||
|
return {
|
||||||
|
runtime: {
|
||||||
|
running: status.running,
|
||||||
|
phase: status.phase,
|
||||||
|
phaseAt: status.phaseAt,
|
||||||
|
inFlight: status.inFlight,
|
||||||
|
tickCount: status.tickCount,
|
||||||
|
currentRunId: status.currentRunId,
|
||||||
|
lastTriggerReason: status.lastTriggerReason,
|
||||||
|
nextRunAt: status.nextRunAt,
|
||||||
|
lastTickAt: status.lastTickAt,
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
enabled: status.enabled,
|
||||||
|
name: status.name,
|
||||||
|
model: status.model,
|
||||||
|
ollamaUrl: status.ollamaUrl,
|
||||||
|
gateIntervalMs: status.gateIntervalMs,
|
||||||
|
heartbeatMs: status.heartbeatMs,
|
||||||
|
observeOnly: status.observeOnly,
|
||||||
|
promptPath: status.promptPath,
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
systemPrompt: status.lastSystemPrompt,
|
||||||
|
stateUpdate: status.lastStateUpdate,
|
||||||
|
transcript: status.lastTranscript,
|
||||||
|
availableTools: status.lastAvailableTools,
|
||||||
|
blockedTools: status.lastBlockedTools,
|
||||||
|
modelMessages: status.lastModelMessages,
|
||||||
|
modelInputAt: status.lastModelInputAt,
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
raw: status.lastModelRawOutput,
|
||||||
|
normalized: status.lastDecision,
|
||||||
|
outputAt: status.lastModelOutputAt,
|
||||||
|
outcome: status.lastOutcome,
|
||||||
|
reason: status.lastReason,
|
||||||
|
},
|
||||||
|
timings: {
|
||||||
|
lastGenerationMs: status.lastGenerationMs,
|
||||||
|
avgGenerationMs: status.avgGenerationMs,
|
||||||
|
generationCount: status.generationCount,
|
||||||
|
},
|
||||||
|
errors: {
|
||||||
|
message: status.lastError,
|
||||||
|
details: status.lastErrorDetails,
|
||||||
|
failedAt: status.lastFailedAt,
|
||||||
|
},
|
||||||
|
history: runHistory,
|
||||||
|
debug: {
|
||||||
|
status,
|
||||||
|
},
|
||||||
|
controls: {
|
||||||
|
supportedActions: ['clearHistory'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDecision(rawContent = '') {
|
||||||
|
const raw = typeof rawContent === 'string' ? rawContent : '';
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
if (!trimmed) return { raw, decision: 'SKIP' };
|
||||||
|
const first = trimmed.split(/\r?\n/).map((line) => line.trim()).find(Boolean) || '';
|
||||||
|
const upper = first.toUpperCase();
|
||||||
|
if (['SKIP', 'CHAT', 'ACTION', 'ACTION+CHAT'].includes(upper)) {
|
||||||
|
return { raw, decision: upper };
|
||||||
|
}
|
||||||
|
return { raw, decision: 'CHAT' };
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFailureInfo(err) {
|
||||||
|
const message = err?.message || String(err || 'Unknown error');
|
||||||
|
const details = {
|
||||||
|
name: err?.name || null,
|
||||||
|
code: err?.code || null,
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
message,
|
||||||
|
details,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
isAdminRole,
|
||||||
|
buildAdminState,
|
||||||
|
normalizeDecision,
|
||||||
|
buildFailureInfo,
|
||||||
|
};
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
signature: 'chat_say(text)',
|
||||||
|
availability() {
|
||||||
|
return { available: true, reason: null };
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
module.exports = {
|
||||||
|
signature: 'ha_set_entity(entity_id, state)',
|
||||||
|
availability(ctx = {}) {
|
||||||
|
const mode = String(ctx.mode || '');
|
||||||
|
if (mode === 'admin' || mode === 'lockdown') {
|
||||||
|
return { available: false, reason: `policy_lock:site_mode_${mode}` };
|
||||||
|
}
|
||||||
|
if (ctx.homeAssistantState?.lightPolicy?.lockedOn) {
|
||||||
|
return { available: false, reason: 'policy_lock:lights_locked_on' };
|
||||||
|
}
|
||||||
|
if (!ctx.homeAssistantState?.connected) return { available: false, reason: 'unavailable' };
|
||||||
|
return { available: true, reason: null };
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
const chatSay = require('./chatSay');
|
||||||
|
const memoryRead = require('./memoryRead');
|
||||||
|
const memoryWrite = require('./memoryWrite');
|
||||||
|
const liftUp = require('./liftUp');
|
||||||
|
const liftDown = require('./liftDown');
|
||||||
|
const neatoStart = require('./neatoStart');
|
||||||
|
const neatoSendHome = require('./neatoSendHome');
|
||||||
|
const neatoLocate = require('./neatoLocate');
|
||||||
|
const neatoClearErrors = require('./neatoClearErrors');
|
||||||
|
const haSetEntity = require('./haSetEntity');
|
||||||
|
|
||||||
|
const TOOL_DEFINITIONS = [
|
||||||
|
chatSay,
|
||||||
|
memoryRead,
|
||||||
|
memoryWrite,
|
||||||
|
liftUp,
|
||||||
|
liftDown,
|
||||||
|
neatoStart,
|
||||||
|
neatoSendHome,
|
||||||
|
neatoLocate,
|
||||||
|
neatoClearErrors,
|
||||||
|
haSetEntity,
|
||||||
|
];
|
||||||
|
|
||||||
|
function evaluateTools(context = {}) {
|
||||||
|
const available = [];
|
||||||
|
const blocked = [];
|
||||||
|
TOOL_DEFINITIONS.forEach((tool) => {
|
||||||
|
const result = typeof tool.availability === 'function' ? tool.availability(context) : { available: false, reason: 'unavailable' };
|
||||||
|
if (result?.available) {
|
||||||
|
available.push(tool.signature);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
blocked.push({ tool: tool.signature, reason: result?.reason || 'unavailable' });
|
||||||
|
});
|
||||||
|
return { available, blocked };
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
TOOL_DEFINITIONS,
|
||||||
|
evaluateTools,
|
||||||
|
};
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
module.exports = {
|
||||||
|
signature: 'lift_down()',
|
||||||
|
availability(ctx = {}) {
|
||||||
|
const mode = String(ctx.mode || '');
|
||||||
|
if (mode === 'admin' || mode === 'lockdown') {
|
||||||
|
return { available: false, reason: `policy_lock:site_mode_${mode}` };
|
||||||
|
}
|
||||||
|
if (!ctx.liftState?.connected) return { available: false, reason: 'unavailable' };
|
||||||
|
if (ctx.liftState?.busy) return { available: false, reason: 'busy' };
|
||||||
|
return { available: true, reason: null };
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
module.exports = {
|
||||||
|
signature: 'lift_up()',
|
||||||
|
availability(ctx = {}) {
|
||||||
|
const mode = String(ctx.mode || '');
|
||||||
|
if (mode === 'admin' || mode === 'lockdown') {
|
||||||
|
return { available: false, reason: `policy_lock:site_mode_${mode}` };
|
||||||
|
}
|
||||||
|
if (!ctx.liftState?.connected) return { available: false, reason: 'unavailable' };
|
||||||
|
if (ctx.liftState?.busy) return { available: false, reason: 'busy' };
|
||||||
|
return { available: true, reason: null };
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
signature: 'memory_read()',
|
||||||
|
availability() {
|
||||||
|
return { available: true, reason: null };
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
signature: 'memory_write(slot, text)',
|
||||||
|
availability() {
|
||||||
|
return { available: true, reason: null };
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
module.exports = {
|
||||||
|
signature: 'neato_clear_errors()',
|
||||||
|
availability(ctx = {}) {
|
||||||
|
const mode = String(ctx.mode || '');
|
||||||
|
if (mode === 'admin' || mode === 'lockdown') {
|
||||||
|
return { available: false, reason: `policy_lock:site_mode_${mode}` };
|
||||||
|
}
|
||||||
|
if (!ctx.neatoState?.connected) return { available: false, reason: 'unavailable' };
|
||||||
|
return { available: true, reason: null };
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
module.exports = {
|
||||||
|
signature: 'neato_locate()',
|
||||||
|
availability(ctx = {}) {
|
||||||
|
const mode = String(ctx.mode || '');
|
||||||
|
if (mode === 'admin' || mode === 'lockdown') {
|
||||||
|
return { available: false, reason: `policy_lock:site_mode_${mode}` };
|
||||||
|
}
|
||||||
|
if (!ctx.neatoState?.connected) return { available: false, reason: 'unavailable' };
|
||||||
|
return { available: true, reason: null };
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
module.exports = {
|
||||||
|
signature: 'neato_send_home()',
|
||||||
|
availability(ctx = {}) {
|
||||||
|
const mode = String(ctx.mode || '');
|
||||||
|
if (mode === 'admin' || mode === 'lockdown') {
|
||||||
|
return { available: false, reason: `policy_lock:site_mode_${mode}` };
|
||||||
|
}
|
||||||
|
if (!ctx.neatoState?.connected) return { available: false, reason: 'unavailable' };
|
||||||
|
return { available: true, reason: null };
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
module.exports = {
|
||||||
|
signature: 'neato_start()',
|
||||||
|
availability(ctx = {}) {
|
||||||
|
const mode = String(ctx.mode || '');
|
||||||
|
if (mode === 'admin' || mode === 'lockdown') {
|
||||||
|
return { available: false, reason: `policy_lock:site_mode_${mode}` };
|
||||||
|
}
|
||||||
|
if (!ctx.neatoState?.connected) return { available: false, reason: 'unavailable' };
|
||||||
|
return { available: true, reason: null };
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -5,6 +5,7 @@ import { useEffect, useMemo, useState } from 'react';
|
|||||||
import { useSession } from '../../context/SessionContext.jsx';
|
import { useSession } from '../../context/SessionContext.jsx';
|
||||||
import RoverRoster from '../RoverRoster/index.jsx';
|
import RoverRoster from '../RoverRoster/index.jsx';
|
||||||
import LlmCommentaryPanel from './LlmCommentaryPanel.jsx';
|
import LlmCommentaryPanel from './LlmCommentaryPanel.jsx';
|
||||||
|
import OverseerControlPanel from './OverseerControlPanel.jsx';
|
||||||
import ReplaySnapshotHealth from './ReplaySnapshotHealth.jsx';
|
import ReplaySnapshotHealth from './ReplaySnapshotHealth.jsx';
|
||||||
import AdminIpLogPanel from './AdminIpLogPanel.jsx';
|
import AdminIpLogPanel from './AdminIpLogPanel.jsx';
|
||||||
|
|
||||||
@@ -28,14 +29,17 @@ export default function AdminPanelContent() {
|
|||||||
setAudioLevels,
|
setAudioLevels,
|
||||||
setPrivateSafety,
|
setPrivateSafety,
|
||||||
llmControl,
|
llmControl,
|
||||||
|
overseerControl,
|
||||||
adminLogs,
|
adminLogs,
|
||||||
llmCommentaryState,
|
llmCommentaryState,
|
||||||
|
overseerControlState,
|
||||||
} = useSession();
|
} = useSession();
|
||||||
const roster = useMemo(() => session?.roster ?? [], [session?.roster]);
|
const roster = useMemo(() => session?.roster ?? [], [session?.roster]);
|
||||||
const [lockStates, setLockStates] = useState({});
|
const [lockStates, setLockStates] = useState({});
|
||||||
const [rebootStates, setRebootStates] = useState({});
|
const [rebootStates, setRebootStates] = useState({});
|
||||||
const [serverRebooting, setServerRebooting] = useState(false);
|
const [serverRebooting, setServerRebooting] = useState(false);
|
||||||
const [clearingLlmHistory, setClearingLlmHistory] = useState(false);
|
const [clearingLlmHistory, setClearingLlmHistory] = useState(false);
|
||||||
|
const [clearingOverseerHistory, setClearingOverseerHistory] = useState(false);
|
||||||
const health = session?.health || null;
|
const health = session?.health || null;
|
||||||
const currentGoal = session?.globalObjective?.text || '';
|
const currentGoal = session?.globalObjective?.text || '';
|
||||||
const goalUpdatedAt = session?.globalObjective?.updatedAt || null;
|
const goalUpdatedAt = session?.globalObjective?.updatedAt || null;
|
||||||
@@ -126,6 +130,19 @@ export default function AdminPanelContent() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleClearOverseerHistory = async () => {
|
||||||
|
const ok = window.confirm('Clear Overseer Control history now?');
|
||||||
|
if (!ok) return;
|
||||||
|
setClearingOverseerHistory(true);
|
||||||
|
try {
|
||||||
|
await overseerControl('clearHistory');
|
||||||
|
} catch (err) {
|
||||||
|
alert(err.message);
|
||||||
|
} finally {
|
||||||
|
setClearingOverseerHistory(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleGoalSave = async () => {
|
const handleGoalSave = async () => {
|
||||||
try {
|
try {
|
||||||
await setGlobalObjective(goalDraft);
|
await setGlobalObjective(goalDraft);
|
||||||
@@ -491,6 +508,11 @@ export default function AdminPanelContent() {
|
|||||||
onClearHistory={handleClearLlmHistory}
|
onClearHistory={handleClearLlmHistory}
|
||||||
clearingHistory={clearingLlmHistory}
|
clearingHistory={clearingLlmHistory}
|
||||||
/>
|
/>
|
||||||
|
<OverseerControlPanel
|
||||||
|
state={overseerControlState}
|
||||||
|
onClearHistory={handleClearOverseerHistory}
|
||||||
|
clearingHistory={clearingOverseerHistory}
|
||||||
|
/>
|
||||||
<AdminIpLogPanel entries={adminLogs} />
|
<AdminIpLogPanel entries={adminLogs} />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export default function OverseerControlPanel({ state, onClearHistory, clearingHistory }) {
|
||||||
|
const [showPayload, setShowPayload] = useState(false);
|
||||||
|
if (!state) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<div className="panel-muted text-xs uppercase">Overseer Control</div>
|
||||||
|
<div className="surface text-xs text-slate-300">No status received yet.</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const runtime = state.runtime || {};
|
||||||
|
const cfg = state.config || {};
|
||||||
|
const output = state.output || {};
|
||||||
|
const timings = state.timings || {};
|
||||||
|
const input = state.input || {};
|
||||||
|
const errors = state.errors || {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<div className="panel-muted text-xs uppercase">Overseer Control</div>
|
||||||
|
<div className="flex gap-0.5 text-xs">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClearHistory}
|
||||||
|
disabled={Boolean(clearingHistory)}
|
||||||
|
className="button-danger disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
|
>
|
||||||
|
{clearingHistory ? 'Clearing...' : 'Clear Overseer History'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="surface flex flex-wrap gap-0.5 text-xs">
|
||||||
|
<span className="surface-muted">running: {runtime.running ? 'yes' : 'no'}</span>
|
||||||
|
<span className="surface-muted">phase: {runtime.phase || '--'}</span>
|
||||||
|
<span className="surface-muted">tick: {runtime.tickCount ?? 0}</span>
|
||||||
|
<span className="surface-muted">trigger: {runtime.lastTriggerReason || '--'}</span>
|
||||||
|
<span className="surface-muted">name: {cfg.name || '--'}</span>
|
||||||
|
<span className="surface-muted">model: {cfg.model || '--'}</span>
|
||||||
|
<span className="surface-muted">observeOnly: {cfg.observeOnly ? 'yes' : 'no'}</span>
|
||||||
|
<span className="surface-muted">decision: {output.normalized || '--'}</span>
|
||||||
|
<span className="surface-muted">outcome: {output.outcome || '--'}</span>
|
||||||
|
<span className="surface-muted">reason: {output.reason || '--'}</span>
|
||||||
|
<span className="surface-muted">last gen: {timings.lastGenerationMs != null ? `${timings.lastGenerationMs}ms` : '--'}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<details className="surface text-xs text-slate-200" open>
|
||||||
|
<summary className="cursor-pointer select-none text-slate-300">Latest Context</summary>
|
||||||
|
<pre className="mt-0.5 whitespace-pre-wrap break-words text-[0.72rem] text-slate-200">
|
||||||
|
{input.stateUpdate || '<none>'}
|
||||||
|
</pre>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details className="surface text-xs text-slate-200">
|
||||||
|
<summary className="cursor-pointer select-none text-slate-300">Tool Availability</summary>
|
||||||
|
<pre className="mt-0.5 whitespace-pre-wrap break-words text-[0.72rem] text-slate-200">
|
||||||
|
{JSON.stringify({ available: input.availableTools, blocked: input.blockedTools }, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
{errors.message ? <div className="surface text-xs text-red-300">Error: {errors.message}</div> : null}
|
||||||
|
|
||||||
|
<details className="surface text-xs text-slate-200">
|
||||||
|
<summary className="cursor-pointer select-none text-slate-300">Recent Runs</summary>
|
||||||
|
<pre className="mt-0.5 whitespace-pre-wrap break-words text-[0.72rem] text-slate-200">
|
||||||
|
{JSON.stringify(state.history || [], null, 2)}
|
||||||
|
</pre>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<button type="button" className="button-dark text-xs" onClick={() => setShowPayload((v) => !v)}>
|
||||||
|
{showPayload ? 'Hide Full Payload' : 'Show Full Payload'}
|
||||||
|
</button>
|
||||||
|
{showPayload ? (
|
||||||
|
<pre className="surface whitespace-pre-wrap break-words text-[0.72rem] text-slate-200">{JSON.stringify(state, null, 2)}</pre>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ const INITIAL_STATE = {
|
|||||||
adminLogs: [],
|
adminLogs: [],
|
||||||
llmCommentaryState: null,
|
llmCommentaryState: null,
|
||||||
llmCommentaryStatus: null,
|
llmCommentaryStatus: null,
|
||||||
|
overseerControlState: null,
|
||||||
alerts: [],
|
alerts: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -128,6 +129,10 @@ export function SessionProvider({ children }) {
|
|||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
function handleOverseerState(payload = null) {
|
||||||
|
const state = payload && typeof payload === 'object' ? payload : null;
|
||||||
|
setState((prev) => ({ ...prev, overseerControlState: state }));
|
||||||
|
}
|
||||||
function handleNeatoLidar(payload = null) {
|
function handleNeatoLidar(payload = null) {
|
||||||
const next = payload && typeof payload === 'object' ? payload : null;
|
const next = payload && typeof payload === 'object' ? payload : null;
|
||||||
setState((prev) => ({ ...prev, neatoLidar: next }));
|
setState((prev) => ({ ...prev, neatoLidar: next }));
|
||||||
@@ -139,6 +144,7 @@ export function SessionProvider({ children }) {
|
|||||||
socket.on('adminlog:init', handleAdminLogInit);
|
socket.on('adminlog:init', handleAdminLogInit);
|
||||||
socket.on('adminlog:entry', handleAdminLogEntry);
|
socket.on('adminlog:entry', handleAdminLogEntry);
|
||||||
socket.on('llm:state', handleLlmState);
|
socket.on('llm:state', handleLlmState);
|
||||||
|
socket.on('overseer:state', handleOverseerState);
|
||||||
socket.on('alert:new', handleAlertNew);
|
socket.on('alert:new', handleAlertNew);
|
||||||
return () => {
|
return () => {
|
||||||
socket.off('session:sync', handleSession);
|
socket.off('session:sync', handleSession);
|
||||||
@@ -148,6 +154,7 @@ export function SessionProvider({ children }) {
|
|||||||
socket.off('adminlog:init', handleAdminLogInit);
|
socket.off('adminlog:init', handleAdminLogInit);
|
||||||
socket.off('adminlog:entry', handleAdminLogEntry);
|
socket.off('adminlog:entry', handleAdminLogEntry);
|
||||||
socket.off('llm:state', handleLlmState);
|
socket.off('llm:state', handleLlmState);
|
||||||
|
socket.off('overseer:state', handleOverseerState);
|
||||||
socket.off('alert:new', handleAlertNew);
|
socket.off('alert:new', handleAlertNew);
|
||||||
};
|
};
|
||||||
}, [setState, socket]);
|
}, [setState, socket]);
|
||||||
@@ -204,6 +211,8 @@ export function SessionProvider({ children }) {
|
|||||||
emitWithAck('session:privateSafety:set', { roverId, safety }),
|
emitWithAck('session:privateSafety:set', { roverId, safety }),
|
||||||
llmControl: (action, controls = {}) =>
|
llmControl: (action, controls = {}) =>
|
||||||
emitWithAck('llm:control', { controls: { action, ...controls } }),
|
emitWithAck('llm:control', { controls: { action, ...controls } }),
|
||||||
|
overseerControl: (action, controls = {}) =>
|
||||||
|
emitWithAck('overseer:control', { controls: { action, ...controls } }),
|
||||||
pushAlert: (alert) =>
|
pushAlert: (alert) =>
|
||||||
setState((prev) => ({
|
setState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
|||||||
Reference in New Issue
Block a user