mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
overseer disableable
This commit is contained in:
@@ -15,6 +15,9 @@ llmCommentary:
|
||||
frequency: 120000
|
||||
overseerControl:
|
||||
enabled: false
|
||||
# autonomous runs the existing vote-gated loop forever; directAddress only
|
||||
# runs one cycle when a chat message starts with the configured name.
|
||||
mode: "autonomous"
|
||||
observeOnly: true
|
||||
postToolsOnlyMessages: false
|
||||
tiebreakerEnable: false
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -18,8 +18,8 @@
|
||||
<script defer src="https://analytics.otter.land/script.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land"></script>
|
||||
<script defer src="https://analytics.otter.land/recorder.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land" data-sample-rate="0.15" data-mask-level="moderate" data-max-duration="300000"></script>
|
||||
<title>Roomba Rover</title>
|
||||
<script type="module" crossorigin src="/assets/index-BDg4B9Q_.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Ch_aZGnW.css">
|
||||
<script type="module" crossorigin src="/assets/index-4TYOvsyF.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CmnedUzQ.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -32,9 +32,16 @@ const { loadMemory, saveMemory, createDefaultMemory, summarizeMemory } = require
|
||||
|
||||
const config = loadConfig();
|
||||
const overseerConfig = config.overseerControl || {};
|
||||
const RUN_MODE_AUTONOMOUS = 'autonomous';
|
||||
const RUN_MODE_DIRECT_ADDRESS = 'directAddress';
|
||||
const RUN_MODES = new Set([RUN_MODE_AUTONOMOUS, RUN_MODE_DIRECT_ADDRESS]);
|
||||
const enabled = Boolean(overseerConfig.enabled);
|
||||
const observeOnly = overseerConfig.observeOnly !== false;
|
||||
const name = String(overseerConfig.name || DEFAULT_NAME).trim() || DEFAULT_NAME;
|
||||
const configuredRunMode = String(overseerConfig.mode || RUN_MODE_AUTONOMOUS).trim();
|
||||
const runMode = RUN_MODES.has(configuredRunMode) ? configuredRunMode : RUN_MODE_AUTONOMOUS;
|
||||
const autonomousMode = runMode === RUN_MODE_AUTONOMOUS;
|
||||
const directAddressMode = runMode === RUN_MODE_DIRECT_ADDRESS;
|
||||
const model = String(overseerConfig.model || '').trim();
|
||||
const ollamaUrl = String(overseerConfig.ollamaUrl || overseerConfig.ollamaServer || '').trim();
|
||||
const gateIntervalMs = normalizeMs(Number(overseerConfig.gateIntervalMs), DEFAULT_GATE_INTERVAL_MS);
|
||||
@@ -52,12 +59,14 @@ const runtime = {
|
||||
generationTotalMs: 0,
|
||||
runHistory: [],
|
||||
liveToolCalls: [],
|
||||
pendingDirectAddressRun: false,
|
||||
memoryStore: loadMemory(),
|
||||
contextResetAt: Date.now(),
|
||||
};
|
||||
|
||||
let status = {
|
||||
enabled,
|
||||
runMode,
|
||||
observeOnly,
|
||||
name,
|
||||
model,
|
||||
@@ -138,12 +147,24 @@ function buildVoteStatus() {
|
||||
gatePassed = yesCount > noCount;
|
||||
}
|
||||
return {
|
||||
// This flag is part of the public vote snapshot because the browser needs
|
||||
// to distinguish "the service is disabled in server config" from "the
|
||||
// service is available but currently stopped by votes or lockdown mode."
|
||||
// Without this explicit value, the UI can only infer stopped/running state
|
||||
// and will keep showing vote controls that cannot actually start anything.
|
||||
enabled,
|
||||
runMode,
|
||||
// Voting is only meaningful for the autonomous scheduler. Direct-address
|
||||
// mode is server-controlled and runs only after a user intentionally starts
|
||||
// a chat message with the configured overseer name, so the public vote UI
|
||||
// should hide instead of offering controls that do not affect execution.
|
||||
votingEnabled: Boolean(enabled && autonomousMode),
|
||||
yesCount,
|
||||
noCount,
|
||||
onlineCount,
|
||||
eligibleCount,
|
||||
gatePassed,
|
||||
running: Boolean(enabled && gatePassed && getMode() !== MODES.LOCKDOWN),
|
||||
running: Boolean(enabled && autonomousMode && gatePassed && getMode() !== MODES.LOCKDOWN),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -264,6 +285,23 @@ function normalizeChatDraft(text) {
|
||||
return next;
|
||||
}
|
||||
|
||||
function isConfiguredNameAddress(text) {
|
||||
const clean = String(text || '').trimStart();
|
||||
if (!clean) return false;
|
||||
|
||||
const lowerText = clean.toLowerCase();
|
||||
const lowerName = name.toLowerCase();
|
||||
if (!lowerText.startsWith(lowerName)) return false;
|
||||
|
||||
const nextChar = clean.charAt(name.length);
|
||||
if (!nextChar) return true;
|
||||
|
||||
// The character after the configured name must separate the invocation from
|
||||
// the rest of the message. This keeps "The Overseer, help" and "the overseer
|
||||
// help" valid while preventing accidental triggers such as "The Overseerish".
|
||||
return /[\s,.:;!?-]/.test(nextChar);
|
||||
}
|
||||
|
||||
function summarizeResult(result) {
|
||||
if (!result || typeof result !== 'object') return null;
|
||||
if (Object.prototype.hasOwnProperty.call(result, 'ok')) return { ok: Boolean(result.ok) };
|
||||
@@ -535,6 +573,77 @@ async function tick() {
|
||||
}
|
||||
}
|
||||
|
||||
function getDirectAddressSkipReason() {
|
||||
if (!enabled) return 'overseerControl.enabled is false';
|
||||
if (!directAddressMode) return 'overseerControl.mode is not directAddress';
|
||||
if (getMode() === MODES.LOCKDOWN) return 'paused during lockdown';
|
||||
return null;
|
||||
}
|
||||
|
||||
async function runDirectAddressCycle(triggerReason = 'direct_address') {
|
||||
const skipReason = getDirectAddressSkipReason();
|
||||
if (skipReason) {
|
||||
updateStatus({
|
||||
lastOutcome: 'skipped',
|
||||
lastReason: skipReason,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (runtime.inFlight) {
|
||||
// Direct-address mode should not create overlapping Ollama requests. If a
|
||||
// user addresses the overseer while a generation is active, remember that a
|
||||
// follow-up pass is needed and collapse any additional mentions into that
|
||||
// single pending pass. This keeps chat responsive without stampeding the
|
||||
// local model server.
|
||||
runtime.pendingDirectAddressRun = true;
|
||||
updateStatus({
|
||||
lastOutcome: 'queued',
|
||||
lastReason: 'direct-address request queued while model request is in flight',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
do {
|
||||
runtime.pendingDirectAddressRun = false;
|
||||
const startedAt = Date.now();
|
||||
runtime.tickCount += 1;
|
||||
runtime.inFlight = true;
|
||||
updateStatus({
|
||||
running: true,
|
||||
inFlight: true,
|
||||
tickCount: runtime.tickCount,
|
||||
lastTickAt: startedAt,
|
||||
phase: 'direct_address_tick',
|
||||
lastTriggerReason: triggerReason,
|
||||
nextRunAt: null,
|
||||
});
|
||||
|
||||
try {
|
||||
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({
|
||||
running: false,
|
||||
inFlight: false,
|
||||
currentRunId: null,
|
||||
nextRunAt: null,
|
||||
phase: getDirectAddressSkipReason() ? 'paused' : 'idle',
|
||||
});
|
||||
}
|
||||
} while (runtime.pendingDirectAddressRun && !getDirectAddressSkipReason());
|
||||
}
|
||||
|
||||
function emitStateToSocket(socket) {
|
||||
if (!socket || !isAdminRole(getRole(socket))) return;
|
||||
socket.emit('overseer:state', buildAdminState(status, runtime.runHistory));
|
||||
@@ -584,10 +693,14 @@ function stopScheduler(reason = 'paused') {
|
||||
}
|
||||
updateStatus({
|
||||
running: false,
|
||||
inFlight: false,
|
||||
currentRunId: null,
|
||||
// Stopping the scheduler cancels future work, but an already-started model
|
||||
// request may still be finishing. Preserve the in-flight flag/current run
|
||||
// so admin state does not briefly claim the service is idle while a direct
|
||||
// or autonomous decision is still awaiting Ollama.
|
||||
inFlight: runtime.inFlight,
|
||||
currentRunId: runtime.inFlight ? status.currentRunId : null,
|
||||
nextRunAt: null,
|
||||
phase: 'paused',
|
||||
phase: runtime.inFlight ? status.phase : 'paused',
|
||||
lastOutcome: 'paused',
|
||||
lastReason: reason,
|
||||
});
|
||||
@@ -610,6 +723,10 @@ function evaluateSchedulerGate(reason = 'gate reevaluated') {
|
||||
stopScheduler('overseerControl.enabled is false');
|
||||
return;
|
||||
}
|
||||
if (directAddressMode) {
|
||||
stopScheduler('direct-address mode waits for configured name mention');
|
||||
return;
|
||||
}
|
||||
if (getMode() === MODES.LOCKDOWN) {
|
||||
stopScheduler('paused during lockdown');
|
||||
return;
|
||||
@@ -643,9 +760,14 @@ roleEvents.on('change', ({ socket }) => {
|
||||
});
|
||||
subscribe('chat:message', ({ payload } = {}) => {
|
||||
const text = String(payload?.text || '').trim();
|
||||
if (text !== 'CLEAR') return;
|
||||
if (payload?.bot) return;
|
||||
clearHistory('chat CLEAR command', { resetPersistentMemory: false, sendConfirmation: true });
|
||||
if (text === 'CLEAR') {
|
||||
clearHistory('chat CLEAR command', { resetPersistentMemory: false, sendConfirmation: true });
|
||||
return;
|
||||
}
|
||||
if (!directAddressMode) return;
|
||||
if (!isConfiguredNameAddress(text)) return;
|
||||
void runDirectAddressCycle('direct_address_chat');
|
||||
});
|
||||
verificationEvents.on('change', () => evaluateSchedulerGate('online vote update'));
|
||||
homeAssistantEvents.on('update', () => updateStatus({ phase: status.phase }));
|
||||
@@ -669,9 +791,12 @@ if (!enabled) {
|
||||
if (getMode() === MODES.LOCKDOWN) {
|
||||
stopScheduler('paused during lockdown');
|
||||
logger.info('overseerControl paused on startup due to lockdown mode');
|
||||
} else if (directAddressMode) {
|
||||
evaluateSchedulerGate('direct-address mode');
|
||||
logger.info('overseerControl direct-address mode enabled', { model, ollamaUrl, observeOnly, name });
|
||||
} else {
|
||||
evaluateSchedulerGate(observeOnly ? 'observe-only mode' : null);
|
||||
logger.info('overseerControl enabled', { model, ollamaUrl, gateIntervalMs, observeOnly });
|
||||
logger.info('overseerControl enabled', { model, ollamaUrl, gateIntervalMs, observeOnly, runMode });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ function buildAdminState(status, runHistory) {
|
||||
},
|
||||
config: {
|
||||
enabled: status.enabled,
|
||||
runMode: status.runMode,
|
||||
name: status.name,
|
||||
model: status.model,
|
||||
ollamaUrl: status.ollamaUrl,
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
5. actual barcode stuff fun
|
||||
1. games
|
||||
1. all games are global and initiated by a user
|
||||
1. all games will require you to scan yourself to keep going sometimes too
|
||||
2. only one game at a time
|
||||
2. scan quest mode, scan 2 items in specific order or something
|
||||
3. most scanned items
|
||||
1. global counters
|
||||
2. you have to
|
||||
5. barcode wiki links
|
||||
6. implement multitabbing prevention using the identity system
|
||||
7. overseer improvements
|
||||
1. make it able to see more stuff
|
||||
|
||||
+21
-2
@@ -122,6 +122,14 @@ function MobileFeatureTabs({
|
||||
ownAudioForward?.source === 'upload' &&
|
||||
ownAudioForward?.state === 'playing',
|
||||
);
|
||||
const showOverseerPreferencePanel = useSessionSelector((state) => {
|
||||
const vote = state.session?.overseerVote;
|
||||
|
||||
// Match the panel's server-owned voting gate so the mobile chat row can
|
||||
// collapse to a single-column layout when voting is unavailable, including
|
||||
// disabled service and direct-address mode.
|
||||
return Boolean(vote?.votingEnabled);
|
||||
});
|
||||
return (
|
||||
<section className="text-base">
|
||||
<Tabs defaultTab="chat" currentTab={activeTab} onTabChange={setActiveTab}>
|
||||
@@ -150,8 +158,19 @@ function MobileFeatureTabs({
|
||||
<div className={`grid ${themeGapClass} md:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)]`}>
|
||||
<SocialButtonsGrid />
|
||||
</div>
|
||||
<div className={`grid ${themeGapClass} grid-cols-[minmax(0,1fr)_minmax(0,1fr)]`}>
|
||||
<OverseerPreferencePanel />
|
||||
<div
|
||||
className={`grid ${themeGapClass} ${
|
||||
showOverseerPreferencePanel
|
||||
? 'grid-cols-[minmax(0,1fr)_minmax(0,1fr)]'
|
||||
: 'grid-cols-[minmax(0,1fr)]'
|
||||
}`}
|
||||
>
|
||||
{/*
|
||||
The overseer vote panel is server-vote-gated. When voting
|
||||
is unavailable, the raw user pile should reclaim the row
|
||||
instead of sitting in a half-empty two-column layout.
|
||||
*/}
|
||||
{showOverseerPreferencePanel ? <OverseerPreferencePanel /> : null}
|
||||
<RawUserPilePanel hideNicknameForm />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -40,7 +40,7 @@ function OverseerMemoryPopup({ memory, onClose }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default function OverseerPreferencePanel() {
|
||||
function OverseerPreferencePanelBody() {
|
||||
const { identifySession } = useSessionActions();
|
||||
const vote = useSessionSelector((state) => state.session?.overseerVote || null);
|
||||
const overseerMemory = useSessionSelector((state) => state.overseerMemory || null);
|
||||
@@ -102,3 +102,21 @@ export default function OverseerPreferencePanel() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function OverseerPreferencePanel() {
|
||||
const visible = useSessionSelector((state) => {
|
||||
const vote = state.session?.overseerVote;
|
||||
|
||||
// The server owns whether voting has any effect. The panel appears only
|
||||
// when the autonomous vote-gated scheduler is active; direct-address mode
|
||||
// is intentionally hidden because users invoke it by starting a chat
|
||||
// message with the configured overseer name instead of voting it on.
|
||||
return Boolean(vote?.votingEnabled);
|
||||
});
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <OverseerPreferencePanelBody />;
|
||||
}
|
||||
|
||||
@@ -165,6 +165,14 @@ export default function RightPaneTabs({ layout, onOpenHelpOverlay }) {
|
||||
ownAudioForward?.source === 'upload' &&
|
||||
ownAudioForward?.state === 'playing',
|
||||
);
|
||||
const showOverseerPreferencePanel = useSessionSelector((state) => {
|
||||
const vote = state.session?.overseerVote;
|
||||
|
||||
// Match the panel's server-owned voting gate so the measured desktop chat
|
||||
// dock can give the side-column height to the user pile when voting is
|
||||
// unavailable, including disabled service and direct-address mode.
|
||||
return Boolean(vote?.votingEnabled);
|
||||
});
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const chatDock = chatDockRef.current;
|
||||
@@ -304,8 +312,20 @@ export default function RightPaneTabs({ layout, onOpenHelpOverlay }) {
|
||||
style={{ height: `${chatDockHeight}px` }}
|
||||
>
|
||||
<ChatPanel fillHeight />
|
||||
<div className={`grid min-h-0 ${themeGapClass} grid-rows-[auto_minmax(0,1fr)]`}>
|
||||
<OverseerPreferencePanel />
|
||||
<div
|
||||
className={`grid min-h-0 ${themeGapClass} ${
|
||||
showOverseerPreferencePanel
|
||||
? 'grid-rows-[auto_minmax(0,1fr)]'
|
||||
: 'grid-rows-[minmax(0,1fr)]'
|
||||
}`}
|
||||
>
|
||||
{/*
|
||||
This side column is height-constrained by the measured
|
||||
chat dock row. When overseer voting is unavailable on the
|
||||
server, the user pile becomes the only child and should
|
||||
receive the whole side-column height.
|
||||
*/}
|
||||
{showOverseerPreferencePanel ? <OverseerPreferencePanel /> : null}
|
||||
<RawUserPilePanel compact hideNicknameForm fillHeight />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user