overser memory viewer!

This commit is contained in:
legop3
2026-06-06 03:04:43 -04:00
parent 353481040b
commit 46c9b8aa43
8 changed files with 237 additions and 150 deletions
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
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -11,8 +11,8 @@
<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="Roomba Rover" /> <meta name="apple-mobile-web-app-title" content="Roomba Rover" />
<title>Roomba Rover</title> <title>Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-CZrapRE0.js"></script> <script type="module" crossorigin src="/assets/index-8c6_50As.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-l6FtFm8i.css"> <link rel="stylesheet" crossorigin href="/assets/index-CzvRO_Gd.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
@@ -156,6 +156,27 @@ function updateStatus(patch = {}) {
}); });
} }
function buildMemorySnapshot() {
// The public UI only needs the same compact text summary that the overseer
// already reasons over. Keeping this as text avoids exposing the raw mutable
// store shape as a browser-facing contract.
return {
summary: summarizeMemory(runtime.memoryStore),
updatedAt: Number(runtime.memoryStore?.updatedAt) || Date.now(),
};
}
function emitMemoryToSocket(socket) {
if (!socket) return;
socket.emit('overseer:memory', buildMemorySnapshot());
}
function emitMemoryToAll() {
// Memory writes are rare and user-visible, so broadcast immediately instead
// of waiting for unrelated session sync events to refresh the popup.
io.sockets.sockets.forEach((socket) => emitMemoryToSocket(socket));
}
function pushRun(run = {}) { function pushRun(run = {}) {
runtime.runHistory = [...runtime.runHistory.slice(-(MAX_RUN_HISTORY - 1)), run]; runtime.runHistory = [...runtime.runHistory.slice(-(MAX_RUN_HISTORY - 1)), run];
} }
@@ -391,6 +412,7 @@ async function runDecision(triggerReason) {
}); });
if (result?.memory && typeof result.memory === 'object') { if (result?.memory && typeof result.memory === 'object') {
runtime.memoryStore = saveMemory(result.memory); runtime.memoryStore = saveMemory(result.memory);
emitMemoryToAll();
} }
pushLiveToolCall({ phase: 'ok', tool: action.tool, result }); pushLiveToolCall({ phase: 'ok', tool: action.tool, result });
actionResults.push({ kind: 'tool', tool: action.tool, ok: true, result }); actionResults.push({ kind: 'tool', tool: action.tool, ok: true, result });
@@ -528,6 +550,7 @@ function clearHistory(reason = 'admin requested clear history', options = {}) {
runtime.generationTotalMs = 0; runtime.generationTotalMs = 0;
if (resetPersistentMemory) { if (resetPersistentMemory) {
runtime.memoryStore = saveMemory(createDefaultMemory()); runtime.memoryStore = saveMemory(createDefaultMemory());
emitMemoryToAll();
} }
updateStatus({ updateStatus({
phase: 'idle', phase: 'idle',
@@ -600,6 +623,7 @@ function evaluateSchedulerGate(reason = 'gate reevaluated') {
io.on('connection', (socket) => { io.on('connection', (socket) => {
emitStateToSocket(socket); emitStateToSocket(socket);
emitMemoryToSocket(socket);
evaluateSchedulerGate('online vote update'); evaluateSchedulerGate('online vote update');
socket.on('disconnect', () => evaluateSchedulerGate('online vote update')); socket.on('disconnect', () => evaluateSchedulerGate('online vote update'));
socket.on('overseer:control', ({ controls } = {}, cb = () => {}) => { socket.on('overseer:control', ({ controls } = {}, cb = () => {}) => {
@@ -1,13 +1,53 @@
import { useState } from 'react';
import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx'; import { useSessionActions, useSessionSelector } from '../../context/SessionContext.jsx';
import { useSettingsNamespace } from '../../settings/index.js'; import { useSettingsNamespace } from '../../settings/index.js';
import CardFrame from '../CardFrame/index.jsx'; import CardFrame from '../CardFrame/index.jsx';
function formatUpdatedAt(value) {
const timestamp = Number(value);
if (!Number.isFinite(timestamp) || timestamp <= 0) return null;
const date = new Date(timestamp);
if (Number.isNaN(date.getTime())) return null;
return date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit', second: '2-digit' });
}
function OverseerMemoryPopup({ memory, onClose }) {
const summary = typeof memory?.summary === 'string' && memory.summary.trim() ? memory.summary : 'Memory unavailable.';
const updatedAt = formatUpdatedAt(memory?.updatedAt);
return (
<div
className="fixed inset-0 z-40 flex items-center justify-center bg-black/75 p-1"
onClick={onClose}
>
<div className="w-full max-w-2xl" onClick={(event) => event.stopPropagation()}>
<CardFrame
title="Overseer memory"
meta={updatedAt ? `Updated ${updatedAt}` : null}
actions={
<button type="button" onClick={onClose} className="button-dark px-1 py-0.25 text-[0.75rem]">
Close
</button>
}
bodyClassName="p-0.5 text-xs"
>
{/* The memory is rendered as the server's existing plain-text summary so
this popup stays consistent with the overseer/admin debug display. */}
<pre className="surface max-h-[70vh] overflow-y-auto whitespace-pre-wrap break-words font-mono text-[0.72rem] text-slate-200">{summary}</pre>
</CardFrame>
</div>
</div>
);
}
export default function OverseerPreferencePanel() { export default function OverseerPreferencePanel() {
const { identifySession } = useSessionActions(); const { identifySession } = useSessionActions();
const vote = useSessionSelector((state) => state.session?.overseerVote || null); const vote = useSessionSelector((state) => state.session?.overseerVote || null);
const overseerMemory = useSessionSelector((state) => state.overseerMemory || null);
const { value: profile } = useSettingsNamespace('profile', { nickname: '' }); const { value: profile } = useSettingsNamespace('profile', { nickname: '' });
const { value: identity } = useSettingsNamespace('identity', { cookieUserId: '' }); const { value: identity } = useSettingsNamespace('identity', { cookieUserId: '' });
const { value, save } = useSettingsNamespace('overseerPreference', { enabled: false }); const { value, save } = useSettingsNamespace('overseerPreference', { enabled: false });
const [memoryOpen, setMemoryOpen] = useState(false);
const enabled = Boolean(value?.enabled); const enabled = Boolean(value?.enabled);
const running = Boolean(vote?.running); const running = Boolean(vote?.running);
@@ -21,30 +61,44 @@ export default function OverseerPreferencePanel() {
}); });
}; };
return ( const actions = (
<CardFrame title="Overseer vote" bodyClassName="space-y-0.5 text-center text-xs text-slate-200"> <button
<label type="button"
className={`flex items-center justify-center gap-1.5 rounded px-1 py-0.5 ${ onClick={() => setMemoryOpen(true)}
enabled ? 'bg-emerald-600/80 text-emerald-50' : 'bg-slate-600/70 text-slate-100' className="button-dark px-1 py-0.25 text-[0.75rem]"
}`} title="View overseer memory"
> >
<input type="checkbox" checked={enabled} onChange={onToggle} /> Memory
<span>Your vote: {enabled ? 'Yes' : 'No'}</span> </button>
</label> );
<span className="text-xs">Enable a local LLM in chat</span>
<div className="mt-0.5 text-slate-400"> return (
Yes!: {Number(vote?.yesCount || 0)}, No...: {Number(vote?.noCount || 0)} <>
</div> <CardFrame title="Overseer vote" actions={actions} bodyClassName="space-y-0.5 text-center text-xs text-slate-200">
<div className="mt-0.5 flex justify-center"> <label
<span className={`flex items-center justify-center gap-1.5 rounded px-1 py-0.5 ${
className={`inline-flex items-center rounded px-1.5 py-0.5 text-[0.7rem] font-medium ${ enabled ? 'bg-emerald-600/80 text-emerald-50' : 'bg-slate-600/70 text-slate-100'
running ? 'bg-emerald-600/80 text-emerald-50' : 'bg-slate-600/70 text-slate-100'
}`} }`}
> >
{running ? 'Running!' : 'Stopped by vote.'} <input type="checkbox" checked={enabled} onChange={onToggle} />
</span> <span>Your vote: {enabled ? 'Yes' : 'No'}</span>
</div> </label>
</CardFrame> <span className="text-xs">Enable a local LLM in chat</span>
<div className="mt-0.5 text-slate-400">
Yes!: {Number(vote?.yesCount || 0)}, No...: {Number(vote?.noCount || 0)}
</div>
<div className="mt-0.5 flex justify-center">
<span
className={`inline-flex items-center rounded px-1.5 py-0.5 text-[0.7rem] font-medium ${
running ? 'bg-emerald-600/80 text-emerald-50' : 'bg-slate-600/70 text-slate-100'
}`}
>
{running ? 'Running!' : 'Stopped by vote.'}
</span>
</div>
</CardFrame>
{memoryOpen ? <OverseerMemoryPopup memory={overseerMemory} onClose={() => setMemoryOpen(false)} /> : null}
</>
); );
} }
+9
View File
@@ -13,6 +13,7 @@ const INITIAL_STATE = {
llmCommentaryState: null, llmCommentaryState: null,
llmCommentaryStatus: null, llmCommentaryStatus: null,
overseerControlState: null, overseerControlState: null,
overseerMemory: null,
alerts: [], alerts: [],
}; };
@@ -132,6 +133,12 @@ export function SessionProvider({ children }) {
const state = payload && typeof payload === 'object' ? payload : null; const state = payload && typeof payload === 'object' ? payload : null;
setState((prev) => ({ ...prev, overseerControlState: state })); setState((prev) => ({ ...prev, overseerControlState: state }));
} }
function handleOverseerMemory(payload = null) {
// Overseer memory is intentionally stored separately from session sync so
// the vote panel can refresh as soon as the overseer writes memory.
const memory = payload && typeof payload === 'object' ? payload : null;
setState((prev) => ({ ...prev, overseerMemory: memory }));
}
socket.on('session:sync', handleSession); socket.on('session:sync', handleSession);
socket.on('log:init', handleLogInit); socket.on('log:init', handleLogInit);
socket.on('log:entry', handleLogEntry); socket.on('log:entry', handleLogEntry);
@@ -139,6 +146,7 @@ export function SessionProvider({ children }) {
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('overseer:state', handleOverseerState);
socket.on('overseer:memory', handleOverseerMemory);
socket.on('alert:new', handleAlertNew); socket.on('alert:new', handleAlertNew);
return () => { return () => {
socket.off('session:sync', handleSession); socket.off('session:sync', handleSession);
@@ -148,6 +156,7 @@ export function SessionProvider({ children }) {
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('overseer:state', handleOverseerState);
socket.off('overseer:memory', handleOverseerMemory);
socket.off('alert:new', handleAlertNew); socket.off('alert:new', handleAlertNew);
}; };
}, [setState, socket]); }, [setState, socket]);