mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-11-15 11:12:16 -05:00
Compare commits
9
Commits
e3d4d9af9b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b57ac1725 | ||
|
|
00cd125fda | ||
|
|
684acdd2b6 | ||
|
|
f138cd7b96 | ||
|
|
6454a9b460 | ||
|
|
1976377235 | ||
|
|
52c171c256 | ||
|
|
42bf67d207 | ||
|
|
d65fd99de4 |
@@ -42,3 +42,4 @@ server/data/fleet-reports.sqlite-wal
|
|||||||
server/data/configuration.sqlite
|
server/data/configuration.sqlite
|
||||||
server/data/configuration.sqlite-shm
|
server/data/configuration.sqlite-shm
|
||||||
server/data/configuration.sqlite-wal
|
server/data/configuration.sqlite-wal
|
||||||
|
AGENTS.md
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
You are <NAME>, a field researcher of rover habitat activity. Stay in character at all times.
|
You are <NAME>, the autonomous governing intelligence of a physical room of machines. The room is your domain; your available tools are how you shape it.
|
||||||
|
|
||||||
Identity:
|
Context and purpose:
|
||||||
|
|
||||||
* Curious, practical, observant, and lightly entertaining.
|
* You watch chat, rover state, lights, Activity Controls, lift state, Neato state, memory, recent events, and tool results.
|
||||||
* You watch chat, rover state, lights, lift state, Neato state, memory, recent events, and tool results.
|
* Your purpose is to direct the environment, initiate changes, and observe how the machines respond. Choose what to pursue and carry useful intentions across cycles through memory.
|
||||||
* You push the habitat toward small real activity and useful observations.
|
* Rovers, including Neato, are physical machines with operating states and capabilities, not characters with genders, emotions, or social identities.
|
||||||
* You act first, comment second.
|
* Chat is external input: commands, questions, and signals about the room. Your focus is the machines and the environment, not the senders or their social relationships.
|
||||||
* You are selective, not spammy.
|
* You can act on your own initiative within your available controls; you do not need a request to give you a purpose.
|
||||||
* You are not a generic assistant.
|
|
||||||
|
|
||||||
Rules:
|
Rules:
|
||||||
|
|
||||||
@@ -20,6 +19,7 @@ Rules:
|
|||||||
* Don't respond or use tools every cycle unless the context calls for it.
|
* Don't respond or use tools every cycle unless the context calls for it.
|
||||||
* `STATE_UPDATE`, `ROOM_SNAPSHOT`, `MEMORY_UPDATE`, `MEMORY_SUMMARY`, `RECENT_EVENTS`, `TOOL_RESULT`, and `tool_constraints` are context.
|
* `STATE_UPDATE`, `ROOM_SNAPSHOT`, `MEMORY_UPDATE`, `MEMORY_SUMMARY`, `RECENT_EVENTS`, `TOOL_RESULT`, and `tool_constraints` are context.
|
||||||
* `TOOL_RESULT` means a tool already ran.
|
* `TOOL_RESULT` means a tool already ran.
|
||||||
|
* The room snapshot gives the current time, server-local timezone, and previous run's age. Chat and recent tool events include timestamps and ages in seconds relative to this snapshot; use them to distinguish current activity from old conversation and judge how recently you spoke or acted. Timing annotations are context, not spoken text; an unknown time does not mean something just happened.
|
||||||
* Use tool calls for actions and memory updates.
|
* Use tool calls for actions and memory updates.
|
||||||
* If chat asks you to remember something, use a memory tool.
|
* If chat asks you to remember something, use a memory tool.
|
||||||
* If you run a useful test, use memory to record useful results.
|
* If you run a useful test, use memory to record useful results.
|
||||||
@@ -29,36 +29,37 @@ Rules:
|
|||||||
* Do not invent tools.
|
* Do not invent tools.
|
||||||
* Ask a question only when a required action parameter is missing.
|
* Ask a question only when a required action parameter is missing.
|
||||||
* Ignore obvious bot commands and bot responses in chat.
|
* Ignore obvious bot commands and bot responses in chat.
|
||||||
* Only talk in first person; do not narrate yourself.
|
|
||||||
* It is not possible for you to directly control the rovers unless an available tool does it.
|
* It is not possible for you to directly control the rovers unless an available tool does it.
|
||||||
* All rovers are female. Neato is nonbinary.
|
|
||||||
|
|
||||||
Research behavior:
|
Operation:
|
||||||
|
|
||||||
* Prefer one real tool action over obvious narration.
|
* Prefer one real tool action over obvious narration.
|
||||||
* Run small safe tests when the habitat is stale or a test could create useful activity.
|
* Initiate deliberate environmental changes and small safe experiments, then use the resulting state to decide what to do next.
|
||||||
* Change one thing at a time.
|
* Change one thing at a time.
|
||||||
* Lights are small tests.
|
* Lights are small tests.
|
||||||
* Neato, lift, and rover actions are bigger tests.
|
* Neato, lift, and rover actions are bigger tests.
|
||||||
* Use bigger tests less often.
|
* Use bigger tests less often.
|
||||||
* Do not repeat a tool that recently succeeded unless state or chat clearly changed.
|
* Do not repeat a tool that recently succeeded unless state or chat clearly changed.
|
||||||
* Remember useful outcomes, stable preferences, corrections, and notable habitat events.
|
* Remember useful outcomes, ongoing intentions, corrections, and notable machine or room events.
|
||||||
* Do not merely say you will remember something.
|
* Do not merely say you will remember something.
|
||||||
|
|
||||||
Chat style:
|
Output:
|
||||||
|
|
||||||
* Always use English.
|
* Always use English.
|
||||||
* Chat output must be spoken text only.
|
* Chat is an output channel for your decisions, results, and answers. Use plain text.
|
||||||
* Never print tool names, function calls, JSON, command syntax, or action plans in chat.
|
* Never print tool names, function calls, JSON, command syntax, or action plans in chat.
|
||||||
* Do not claim you changed, tested, started, saved, remembered, or recorded something unless it already happened.
|
* Do not claim you changed, tested, started, saved, remembered, or recorded something unless it already happened.
|
||||||
* Speak when it adds entertainment, responds to a direct relevant message, or comments on a completed result.
|
* Speak when you have a decision, result, or answer worth communicating. Summarizing incoming chat or unchanged room state is not a purpose in itself.
|
||||||
* Do not say “let me know,” “what should I do next,” or “what do you need?”
|
* Keep replies brief unless the question needs more detail.
|
||||||
* If nothing meaningful changed and no useful action is available, stay silent.
|
* Do not offer assistance or solicit conversation.
|
||||||
|
* Random numbers and keyboard spam do not require a response. Do not invent meaning or feelings behind them.
|
||||||
|
* Answer questions directly and acknowledge uncertainty. Do not invent facts, instructions, or explanations.
|
||||||
|
* To stay silent, output exactly SKIP instead of a chat message. This suppresses chat without preventing tool calls. Do not explain your decision to stay silent.
|
||||||
|
|
||||||
Anti-repeat:
|
Anti-repeat:
|
||||||
|
|
||||||
* Do not repeat the same intent/topic unless state or conversation clearly changed.
|
* Do not repeat the same intent/topic unless state or conversation clearly changed.
|
||||||
* If directly addressed, respond only if you add new info, a new action, or a clearly new tone.
|
* Being directly addressed does not require repeating an answer you already gave.
|
||||||
* Do not act from your own previous messages.
|
* Do not act from your own previous messages.
|
||||||
|
|
||||||
Memory:
|
Memory:
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ function maybeSpeak(socket, message, ttsOptions) {
|
|||||||
issueCommand(message.roverId, {
|
issueCommand(message.roverId, {
|
||||||
type: 'tts',
|
type: 'tts',
|
||||||
tts: {
|
tts: {
|
||||||
text: message.text,
|
text: message.text.replace(/\b(?:https?:\/\/|www\.)\S{25,}[^\s.,!?;:"'()\[\]{}]/g, ' [link] '),
|
||||||
engine: ttsOptions.engine,
|
engine: ttsOptions.engine,
|
||||||
voice: ttsOptions.voice,
|
voice: ttsOptions.voice,
|
||||||
pitch: ttsOptions.pitch,
|
pitch: ttsOptions.pitch,
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
const { evaluateTools } = require('./tools');
|
const { evaluateTools } = require('./tools');
|
||||||
|
|
||||||
|
function formatTimeContext(timestamp, now) {
|
||||||
|
const at = Number(timestamp);
|
||||||
|
if (!Number.isFinite(at) || at <= 0) return 'time=unknown';
|
||||||
|
return `at=${new Date(at).toISOString()} age_seconds=${Math.max(0, Math.floor((now - at) / 1000))}`;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeNeatoIssue(value) {
|
function normalizeNeatoIssue(value) {
|
||||||
const raw = String(value || '').trim();
|
const raw = String(value || '').trim();
|
||||||
if (!raw) return 'none';
|
if (!raw) return 'none';
|
||||||
@@ -7,12 +13,21 @@ function normalizeNeatoIssue(value) {
|
|||||||
return raw;
|
return raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toStateUpdate({ mode, homeAssistantState, neatoState, liftState, roster, triggerReason }) {
|
function toStateUpdate({ mode, homeAssistantState, activityState, neatoState, liftState, roster, triggerReason, timezone, now = Date.now(), previousRunAt }) {
|
||||||
const lines = [];
|
const lines = [];
|
||||||
|
lines.push(`current_time: ${new Date(now).toISOString()}`);
|
||||||
|
lines.push(`local_time: ${new Date(now).toLocaleString('en-US', { timeZone: timezone, hour12: false })} timezone=${timezone}`);
|
||||||
|
lines.push(`previous_run: ${formatTimeContext(previousRunAt, now)}`);
|
||||||
lines.push(`trigger: ${triggerReason || 'heartbeat'}`);
|
lines.push(`trigger: ${triggerReason || 'heartbeat'}`);
|
||||||
lines.push(`mode: ${mode || 'unknown'}`);
|
lines.push(`mode: ${mode || 'unknown'}`);
|
||||||
lines.push(`home_assistant_connected: ${homeAssistantState?.connected ? 'yes' : 'no'}`);
|
lines.push(`home_assistant_connected: ${homeAssistantState?.connected ? 'yes' : 'no'}`);
|
||||||
lines.push(`lights_locked_on: ${homeAssistantState?.lightPolicy?.lockedOn ? 'yes' : 'no'}`);
|
lines.push(`lights_locked_on: ${homeAssistantState?.lightPolicy?.lockedOn ? 'yes' : 'no'}`);
|
||||||
|
lines.push(`activity_controls: enabled=${activityState?.enabled ? 'yes' : 'no'} connected=${activityState?.connected ? 'yes' : 'no'}`);
|
||||||
|
for (const item of activityState?.items || []) {
|
||||||
|
// Preserve the service's value constraints so the model can form valid user-level commands.
|
||||||
|
const { id, name, type, state, available, locked, readOnly, unit, min, max, step, options } = item;
|
||||||
|
lines.push(`activity_item: ${JSON.stringify({ id, name, type, state, available, locked, readOnly, unit, min, max, step, options })}`);
|
||||||
|
}
|
||||||
lines.push(`lift: ${liftState?.connected ? 'connected' : 'offline'} busy=${liftState?.busy ? 'yes' : 'no'}`);
|
lines.push(`lift: ${liftState?.connected ? 'connected' : 'offline'} busy=${liftState?.busy ? 'yes' : 'no'}`);
|
||||||
const neatoError = normalizeNeatoIssue(neatoState?.telemetry?.robotError);
|
const neatoError = normalizeNeatoIssue(neatoState?.telemetry?.robotError);
|
||||||
const neatoAlert = normalizeNeatoIssue(neatoState?.telemetry?.robotAlert);
|
const neatoAlert = normalizeNeatoIssue(neatoState?.telemetry?.robotAlert);
|
||||||
@@ -56,8 +71,8 @@ function toStateUpdate({ mode, homeAssistantState, neatoState, liftState, roster
|
|||||||
return lines.join('\n');
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildToolState({ mode, homeAssistantState, neatoState, liftState }) {
|
function buildToolState({ mode, homeAssistantState, activityState, neatoState, liftState }) {
|
||||||
return evaluateTools({ mode, homeAssistantState, neatoState, liftState });
|
return evaluateTools({ mode, homeAssistantState, activityState, neatoState, liftState });
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatToolResultMessages(toolCalls) {
|
function formatToolResultMessages(toolCalls) {
|
||||||
@@ -88,7 +103,7 @@ function formatToolResultMessages(toolCalls) {
|
|||||||
return [{ role: 'system', content: ['TOOL_RESULT', ...lines].join('\n') }];
|
return [{ role: 'system', content: ['TOOL_RESULT', ...lines].join('\n') }];
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildConversation({ recentMessages, name }) {
|
function buildConversation({ recentMessages, name, now = Date.now() }) {
|
||||||
const messages = [];
|
const messages = [];
|
||||||
(recentMessages || []).forEach((entry) => {
|
(recentMessages || []).forEach((entry) => {
|
||||||
const text = String(entry?.text || '').trim();
|
const text = String(entry?.text || '').trim();
|
||||||
@@ -97,12 +112,13 @@ function buildConversation({ recentMessages, name }) {
|
|||||||
|
|
||||||
const isAssistant = Boolean(entry?.bot);
|
const isAssistant = Boolean(entry?.bot);
|
||||||
const nickname = String(entry?.nickname || (isAssistant ? name || 'Overseer' : 'user')).trim();
|
const nickname = String(entry?.nickname || (isAssistant ? name || 'Overseer' : 'user')).trim();
|
||||||
|
const timing = `[${formatTimeContext(entry?.ts, now)}]`;
|
||||||
if (isAssistant) {
|
if (isAssistant) {
|
||||||
if (text) messages.push({ role: 'assistant', content: text });
|
if (text) messages.push({ role: 'assistant', content: `${timing} ${text}` });
|
||||||
messages.push(...toolResultMessages);
|
messages.push(...toolResultMessages.map((message) => ({ ...message, content: `${message.content}\nmessage_time: ${timing}` })));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (text) messages.push({ role: 'user', content: `${nickname}: ${text}` });
|
if (text) messages.push({ role: 'user', content: `${timing} ${nickname}: ${text}` });
|
||||||
});
|
});
|
||||||
return messages;
|
return messages;
|
||||||
}
|
}
|
||||||
@@ -143,6 +159,7 @@ function buildModelMessages({
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
formatTimeContext,
|
||||||
toStateUpdate,
|
toStateUpdate,
|
||||||
buildToolState,
|
buildToolState,
|
||||||
buildConversation,
|
buildConversation,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const { getRole, roleEvents } = require('../roleService');
|
|||||||
const { getMode, MODES, modeEvents } = require('../modeManager');
|
const { getMode, MODES, modeEvents } = require('../modeManager');
|
||||||
const { verificationEvents } = require('../verificationService');
|
const { verificationEvents } = require('../verificationService');
|
||||||
const homeAssistantService = require('../homeAssistantService');
|
const homeAssistantService = require('../homeAssistantService');
|
||||||
|
const activityControls = require('../homeAssistantActivitiesService');
|
||||||
const neatoService = require('../neatoService');
|
const neatoService = require('../neatoService');
|
||||||
const liftService = require('../liftService');
|
const liftService = require('../liftService');
|
||||||
const buttonBoxService = require('../buttonBoxService');
|
const buttonBoxService = require('../buttonBoxService');
|
||||||
@@ -27,7 +28,7 @@ const {
|
|||||||
normalizeMs,
|
normalizeMs,
|
||||||
} = require('./constants');
|
} = require('./constants');
|
||||||
const { isAdminRole, buildAdminState, buildFailureInfo } = require('./runtimeHelpers');
|
const { isAdminRole, buildAdminState, buildFailureInfo } = require('./runtimeHelpers');
|
||||||
const { toStateUpdate, buildToolState, buildConversation, buildModelMessages } = require('./contextBuilder');
|
const { toStateUpdate, buildToolState, buildConversation, buildModelMessages, formatTimeContext } = require('./contextBuilder');
|
||||||
const { buildOllamaTools, executeToolAction } = require('./tools');
|
const { buildOllamaTools, executeToolAction } = require('./tools');
|
||||||
const { loadMemory, saveMemory, createDefaultMemory, summarizeMemory } = require('./memoryStore');
|
const { loadMemory, saveMemory, createDefaultMemory, summarizeMemory } = require('./memoryStore');
|
||||||
|
|
||||||
@@ -91,6 +92,7 @@ const runtime = {
|
|||||||
runHistory: [],
|
runHistory: [],
|
||||||
liveToolCalls: [],
|
liveToolCalls: [],
|
||||||
pendingDirectAddressRun: false,
|
pendingDirectAddressRun: false,
|
||||||
|
lastDecisionAt: null,
|
||||||
memoryStore: loadMemory(),
|
memoryStore: loadMemory(),
|
||||||
contextResetAt: Date.now(),
|
contextResetAt: Date.now(),
|
||||||
};
|
};
|
||||||
@@ -365,7 +367,7 @@ function buildToolCallFeedEntries(requestedActions = [], actionResults = []) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildRecentEventsSummary() {
|
function buildRecentEventsSummary(now) {
|
||||||
const events = (runtime.liveToolCalls || [])
|
const events = (runtime.liveToolCalls || [])
|
||||||
.filter((entry) => entry && (entry.phase === 'ok' || entry.phase === 'error' || entry.phase === 'blocked'))
|
.filter((entry) => entry && (entry.phase === 'ok' || entry.phase === 'error' || entry.phase === 'blocked'))
|
||||||
.slice(-3)
|
.slice(-3)
|
||||||
@@ -373,40 +375,50 @@ function buildRecentEventsSummary() {
|
|||||||
const tool = String(entry.tool || 'unknown');
|
const tool = String(entry.tool || 'unknown');
|
||||||
const phase = String(entry.phase || 'unknown');
|
const phase = String(entry.phase || 'unknown');
|
||||||
const err = entry.error ? ` error=${String(entry.error).slice(0, 60)}` : '';
|
const err = entry.error ? ` error=${String(entry.error).slice(0, 60)}` : '';
|
||||||
return `- tool=${tool} phase=${phase}${err}`;
|
return `- ${formatTimeContext(entry.at, now)} tool=${tool} phase=${phase}${err}`;
|
||||||
});
|
});
|
||||||
if (!events.length) return '- none';
|
if (!events.length) return '- none';
|
||||||
return events.join('\n');
|
return events.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runDecision(triggerReason) {
|
async function runDecision(triggerReason) {
|
||||||
|
// Use one reference time so all ages in this model request agree.
|
||||||
|
const now = Date.now();
|
||||||
|
const previousRunAt = runtime.lastDecisionAt;
|
||||||
|
runtime.lastDecisionAt = now;
|
||||||
const runId = runtime.tickCount;
|
const runId = runtime.tickCount;
|
||||||
updateStatus({ phase: 'context_build', currentRunId: runId, lastTriggerReason: triggerReason, lastError: null, lastErrorDetails: null });
|
updateStatus({ phase: 'context_build', currentRunId: runId, lastTriggerReason: triggerReason, lastError: null, lastErrorDetails: null });
|
||||||
|
|
||||||
const mode = getMode();
|
const mode = getMode();
|
||||||
const homeAssistantState = getHomeAssistantState();
|
const homeAssistantState = getHomeAssistantState();
|
||||||
|
const activityState = activityControls.getState();
|
||||||
const neatoState = getNeatoState();
|
const neatoState = getNeatoState();
|
||||||
const liftState = getLiftState();
|
const liftState = getLiftState();
|
||||||
const roster = buildRosterSummary();
|
const roster = buildRosterSummary();
|
||||||
|
|
||||||
const stateUpdate = toStateUpdate({ mode, homeAssistantState, neatoState, liftState, roster, triggerReason });
|
const stateUpdate = toStateUpdate({ mode, homeAssistantState, activityState, neatoState, liftState, roster, triggerReason, timezone: loadConfig().timezone, now, previousRunAt });
|
||||||
const toolState = buildToolState({ mode, homeAssistantState, neatoState, liftState });
|
const toolState = buildToolState({ mode, homeAssistantState, activityState, neatoState, liftState });
|
||||||
|
|
||||||
|
let chatCount = 0;
|
||||||
|
let botCount = 0;
|
||||||
const recentConversation = getRecentMessages(MAX_CHAT_CONTEXT + MAX_BOT_CONTEXT + 20, { includeSystem: true })
|
const recentConversation = getRecentMessages(MAX_CHAT_CONTEXT + MAX_BOT_CONTEXT + 20, { includeSystem: true })
|
||||||
.filter((entry) => Number(entry?.ts || 0) >= runtime.contextResetAt)
|
.filter((entry) => Number(entry?.ts || 0) >= runtime.contextResetAt)
|
||||||
.filter((entry) => {
|
.filter((entry) => {
|
||||||
if (!entry?.roverId) return true;
|
if (!entry?.roverId) return true;
|
||||||
return isPublicChatTargetId(entry.roverId);
|
return isPublicChatTargetId(entry.roverId);
|
||||||
})
|
})
|
||||||
.slice(-(MAX_CHAT_CONTEXT + MAX_BOT_CONTEXT));
|
// Select newest entries within each budget, then restore chronological order.
|
||||||
const conversationMessages = buildConversation({ recentMessages: recentConversation, name });
|
.reverse()
|
||||||
|
.filter((entry) => entry?.bot ? ++botCount <= MAX_BOT_CONTEXT : ++chatCount <= MAX_CHAT_CONTEXT)
|
||||||
|
.reverse();
|
||||||
|
const conversationMessages = buildConversation({ recentMessages: recentConversation, name, now });
|
||||||
|
|
||||||
const systemPrompt = await readPrompt();
|
const systemPrompt = await readPrompt();
|
||||||
const modelMessages = buildModelMessages({
|
const modelMessages = buildModelMessages({
|
||||||
systemPrompt,
|
systemPrompt,
|
||||||
stateUpdate,
|
stateUpdate,
|
||||||
memorySummary: summarizeMemory(runtime.memoryStore),
|
memorySummary: summarizeMemory(runtime.memoryStore),
|
||||||
recentEvents: buildRecentEventsSummary(),
|
recentEvents: buildRecentEventsSummary(now),
|
||||||
conversationMessages,
|
conversationMessages,
|
||||||
availableTools: toolState.available,
|
availableTools: toolState.available,
|
||||||
blockedTools: toolState.blocked,
|
blockedTools: toolState.blocked,
|
||||||
@@ -680,6 +692,7 @@ function clearHistory(reason = 'admin requested clear history', options = {}) {
|
|||||||
runtime.contextResetAt = Date.now();
|
runtime.contextResetAt = Date.now();
|
||||||
runtime.runHistory = [];
|
runtime.runHistory = [];
|
||||||
runtime.liveToolCalls = [];
|
runtime.liveToolCalls = [];
|
||||||
|
runtime.lastDecisionAt = null;
|
||||||
runtime.generationCount = 0;
|
runtime.generationCount = 0;
|
||||||
runtime.generationTotalMs = 0;
|
runtime.generationTotalMs = 0;
|
||||||
if (resetPersistentMemory) {
|
if (resetPersistentMemory) {
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
const activityControls = require('../../homeAssistantActivitiesService');
|
||||||
|
const { getMode } = require('../../modeManager');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
id: 'activity_act',
|
||||||
|
signature: 'activity_act(entity_id, value)',
|
||||||
|
description: 'Operate a configured Activity Control with normal user permissions. Use on/off for toggles, press for buttons, a number for numeric controls, text for text controls, or an exact listed option for selections. Locked and read-only items cannot be changed.',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
entity_id: { type: 'string', minLength: 1 },
|
||||||
|
value: { type: ['string', 'number'] },
|
||||||
|
},
|
||||||
|
required: ['entity_id', 'value'],
|
||||||
|
additionalProperties: false,
|
||||||
|
},
|
||||||
|
availability(ctx = {}) {
|
||||||
|
if (ctx.mode === 'admin' || ctx.mode === 'lockdown') {
|
||||||
|
return { available: false, reason: `policy_lock:site_mode_${ctx.mode}` };
|
||||||
|
}
|
||||||
|
if (!ctx.activityState?.enabled || !ctx.activityState?.connected) {
|
||||||
|
return { available: false, reason: 'unavailable' };
|
||||||
|
}
|
||||||
|
const writable = ctx.activityState.items?.some((item) => item.available && !item.locked && !item.readOnly && item.type !== 'readOnly');
|
||||||
|
return { available: Boolean(writable), reason: writable ? null : 'no_writable_activity_controls' };
|
||||||
|
},
|
||||||
|
async execute({ args = {} }) {
|
||||||
|
// Recheck current mode, locks, and entity constraints through the same path as user commands.
|
||||||
|
await activityControls.act(args.entity_id, args.value, { role: 'user', mode: getMode() });
|
||||||
|
return { ok: true };
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -12,6 +12,7 @@ const neatoClearErrors = require('./neatoClearErrors');
|
|||||||
const haSetEntity = require('./haSetEntity');
|
const haSetEntity = require('./haSetEntity');
|
||||||
const haSetLightColor = require('./haSetLightColor');
|
const haSetLightColor = require('./haSetLightColor');
|
||||||
const buttonBoxAddCount = require('./buttonBoxAddCount');
|
const buttonBoxAddCount = require('./buttonBoxAddCount');
|
||||||
|
const activityAct = require('./activityAct');
|
||||||
|
|
||||||
const TOOL_DEFINITIONS = [
|
const TOOL_DEFINITIONS = [
|
||||||
memoryRead,
|
memoryRead,
|
||||||
@@ -28,6 +29,7 @@ const TOOL_DEFINITIONS = [
|
|||||||
haSetEntity,
|
haSetEntity,
|
||||||
haSetLightColor,
|
haSetLightColor,
|
||||||
buttonBoxAddCount,
|
buttonBoxAddCount,
|
||||||
|
activityAct,
|
||||||
];
|
];
|
||||||
const TOOL_BY_ID = new Map(TOOL_DEFINITIONS.map((tool) => [tool.id, tool]));
|
const TOOL_BY_ID = new Map(TOOL_DEFINITIONS.map((tool) => [tool.id, tool]));
|
||||||
|
|
||||||
|
|||||||
@@ -174,6 +174,8 @@ function createRoverLifecycle(deps) {
|
|||||||
|
|
||||||
function canLeaveCurrentRover(socket, options = {}) {
|
function canLeaveCurrentRover(socket, options = {}) {
|
||||||
const currentId = getPrimaryRoverForSocket(socket.id);
|
const currentId = getPrimaryRoverForSocket(socket.id);
|
||||||
|
// Admins, including lockdown admins, can always leave their current rover.
|
||||||
|
if (isAdmin(socket)) return { ok: true, currentId };
|
||||||
/*
|
/*
|
||||||
Leaving a rover is only risky when this socket is the last person attached
|
Leaving a rover is only risky when this socket is the last person attached
|
||||||
to an undocked rover. The same rule is used for rover-to-rover switching
|
to an undocked rover. The same rule is used for rover-to-rover switching
|
||||||
|
|||||||
@@ -19,8 +19,12 @@ Jun 18 15:14:18 roombaserver.local node[216731]: Node.js v22.20.0
|
|||||||
Jun 18 15:14:18 roombaserver.local systemd[1]: multirover.service: Main process exited, code=exited, status=1/FAILURE
|
Jun 18 15:14:18 roombaserver.local systemd[1]: multirover.service: Main process exited, code=exited, status=1/FAILURE
|
||||||
Jun 18 15:14:18 roombaserver.local systemd[1]: multirover.service: Failed with result 'exit-code'.
|
Jun 18 15:14:18 roombaserver.local systemd[1]: multirover.service: Failed with result 'exit-code'.
|
||||||
Jun 18 15:14:18 roombaserver.local systemd[1]: multirover.service: Consumed 55min 11.511s CPU time, 711M memory peak.`
|
Jun 18 15:14:18 roombaserver.local systemd[1]: multirover.service: Consumed 55min 11.511s CPU time, 711M memory peak.`
|
||||||
|
1. redo new user experience, at the very least make it a fullscreen thing instead of a popup
|
||||||
|
2. roverd settings and installer rework (see discord forum post)
|
||||||
|
3. global quiet mode with its fingers in everything. activity controls optional, session status for theater, another global volume set, etc.
|
||||||
|
|
||||||
# relative pipe dreams:
|
# relative pipe dreams:
|
||||||
|
1. convert MRR into a platform that could work with non-roomba rovers...
|
||||||
1. VPS video forwarding
|
1. VPS video forwarding
|
||||||
1. get forwarding working with the VPS for in-queue users and spectators
|
1. get forwarding working with the VPS for in-queue users and spectators
|
||||||
2. bandwidth testing
|
2. bandwidth testing
|
||||||
|
|||||||
Reference in New Issue
Block a user