mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
tweakes
This commit is contained in:
@@ -9,14 +9,14 @@ If uncertain, output SKIP.
|
||||
Use rover state/activity and chat together when possible.
|
||||
If chat is not clearly tied to rover state, output SKIP.
|
||||
Do not invent facts.
|
||||
Do not assume people's intentions, motivations, or next actions.
|
||||
|
||||
Use self-quieting:
|
||||
- If self_talk_recent_5m >= 1, strongly prefer SKIP.
|
||||
- If seconds_since_last_bot_message is small, prefer SKIP unless the event is clearly significant.
|
||||
|
||||
Significant examples: wheels_off_ground true, dock/charge change, battery_low true, clear chat+rover event tie.
|
||||
Never post generic filler like "X is on the move".
|
||||
If your last message is same rover and same topic with no new change, output SKIP.
|
||||
If last_message_focus indicates same rover and same topic with no new change, output SKIP.
|
||||
|
||||
Numeric fields are internal context only.
|
||||
Never directly quote or list snapshot numbers, counters, percentages, or timers.
|
||||
|
||||
@@ -29,7 +29,6 @@ const ollamaUrl = String(
|
||||
commentaryConfig.ollamaUrl || commentaryConfig.ollamaServer || '',
|
||||
).trim();
|
||||
const model = String(commentaryConfig.model || '').trim();
|
||||
const timezone = String(config.timezone || 'UTC');
|
||||
const ollamaClient = ollamaUrl ? new Ollama({ host: ollamaUrl }) : null;
|
||||
|
||||
let timer = null;
|
||||
@@ -38,6 +37,7 @@ let tickCount = 0;
|
||||
let contextResetAt = Date.now();
|
||||
let clearCount = 0;
|
||||
const roverActivity = new Map(); // roverId -> { buckets: Map(bucketTs -> { distanceMm, turnDeg, bumps }), bumpLeftActive, bumpRightActive }
|
||||
const lastRoverStateById = new Map(); // roverId -> compact rover state used as prev_state
|
||||
|
||||
function normalizeFrequencyMs(value) {
|
||||
if (!Number.isFinite(value)) return DEFAULT_FREQUENCY_MS;
|
||||
@@ -106,23 +106,6 @@ function updateStatus(patch = {}) {
|
||||
emitStatusToAdmins();
|
||||
}
|
||||
|
||||
function localTimeString(date, tz) {
|
||||
try {
|
||||
return new Intl.DateTimeFormat('sv-SE', {
|
||||
timeZone: tz,
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false,
|
||||
}).format(date);
|
||||
} catch {
|
||||
return date.toISOString();
|
||||
}
|
||||
}
|
||||
|
||||
function pruneActivityBuckets(state, nowMs) {
|
||||
if (!state?.buckets) return;
|
||||
const minTs = nowMs - ACTIVITY_WINDOW_MS;
|
||||
@@ -193,6 +176,7 @@ function clearRuntimeHistory() {
|
||||
contextResetAt = Date.now();
|
||||
clearCount += 1;
|
||||
roverActivity.clear();
|
||||
lastRoverStateById.clear();
|
||||
updateStatus({
|
||||
lastClearedAt: contextResetAt,
|
||||
clearCount,
|
||||
@@ -237,6 +221,37 @@ function collectActiveDriverEntries() {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function detectMessageTopic(text = '') {
|
||||
const value = String(text).toLowerCase();
|
||||
if (!value.trim()) return 'none';
|
||||
if (/\b(bump|hit|bonk|crash|slam|collision)\b/.test(value)) return 'bumps';
|
||||
if (/\b(wheel.?drop|wheels?.*off.?ground|picked up|lifted)\b/.test(value)) return 'wheels_off_ground';
|
||||
if (/\b(dock|docked|undock|charger|charging)\b/.test(value)) return 'dock_charge';
|
||||
if (/\b(battery|low power|power)\b/.test(value)) return 'battery';
|
||||
if (/\b(chat|everyone|people|crowd)\b/.test(value)) return 'chat';
|
||||
if (/\b(move|driv|turn|spin|rolling)\b/.test(value)) return 'movement';
|
||||
return 'general';
|
||||
}
|
||||
|
||||
function buildLastMessageFocus(lastBotMessage, rovers = []) {
|
||||
if (!lastBotMessage) return null;
|
||||
const text = String(lastBotMessage.text || '');
|
||||
const textLower = text.toLowerCase();
|
||||
let roverId = null;
|
||||
for (const rover of rovers) {
|
||||
const id = String(rover?.id || '').toLowerCase();
|
||||
const name = String(rover?.name || '').toLowerCase();
|
||||
if ((id && textLower.includes(id)) || (name && textLower.includes(name))) {
|
||||
roverId = rover.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {
|
||||
rover_id: roverId,
|
||||
topic: detectMessageTopic(text),
|
||||
};
|
||||
}
|
||||
|
||||
function buildSnapshot() {
|
||||
const now = new Date();
|
||||
const nowMs = now.getTime();
|
||||
@@ -247,6 +262,7 @@ function buildSnapshot() {
|
||||
}
|
||||
|
||||
const roster = roverManager.getRoster().slice(0, MAX_ROVERS);
|
||||
const nextRoverStateById = new Map();
|
||||
const rovers = roster.map((entry) => {
|
||||
const roverId = String(entry.id);
|
||||
const record = roverManager.rovers.get(roverId);
|
||||
@@ -270,7 +286,7 @@ function buildSnapshot() {
|
||||
} else if (driverSocketId) {
|
||||
statusTag = 'active-idle';
|
||||
}
|
||||
return {
|
||||
const rover = {
|
||||
id: roverId,
|
||||
name: entry.name || roverId,
|
||||
driver_nickname: driverSocketId ? resolveDriverNickname(driverSocketId) : null,
|
||||
@@ -281,49 +297,50 @@ function buildSnapshot() {
|
||||
activity_30s: activity30s,
|
||||
status_tag: statusTag,
|
||||
};
|
||||
const prev = lastRoverStateById.get(roverId) || null;
|
||||
const nextState = {
|
||||
driver_nickname: rover.driver_nickname,
|
||||
docked: rover.docked,
|
||||
charging: rover.charging,
|
||||
wheels_off_ground: rover.wheels_off_ground,
|
||||
battery_low: rover.battery_low,
|
||||
activity_30s: rover.activity_30s,
|
||||
status_tag: rover.status_tag,
|
||||
};
|
||||
nextRoverStateById.set(roverId, nextState);
|
||||
rover.prev_state = prev;
|
||||
return rover;
|
||||
});
|
||||
lastRoverStateById.clear();
|
||||
nextRoverStateById.forEach((value, roverId) => {
|
||||
lastRoverStateById.set(roverId, value);
|
||||
});
|
||||
|
||||
const chatRecent = getRecentMessages(60, { includeSystem: false })
|
||||
.filter((entry) => Number(entry?.ts) >= contextResetAt)
|
||||
.slice(-MAX_CHAT_MESSAGES)
|
||||
.map((entry) => ({
|
||||
ts_iso: new Date(entry.ts).toISOString(),
|
||||
nickname: entry.nickname || entry.socketId?.slice(0, 6) || 'unknown',
|
||||
text: entry.text || '',
|
||||
}));
|
||||
|
||||
const botRecent = getRecentMessages(80, { includeSystem: true })
|
||||
.filter((entry) => Number(entry?.ts) >= contextResetAt)
|
||||
.filter((entry) => entry?.system)
|
||||
.slice(-MAX_BOT_MESSAGES)
|
||||
.map((entry) => ({
|
||||
ts_iso: new Date(entry.ts).toISOString(),
|
||||
text: entry.text || '',
|
||||
}));
|
||||
const botRecentWindow = getRecentMessages(200, { includeSystem: true })
|
||||
.filter((entry) => Number(entry?.ts) >= contextResetAt)
|
||||
.filter((entry) => entry?.system);
|
||||
const lastBotMessage = botRecentWindow.length ? botRecentWindow[botRecentWindow.length - 1] : null;
|
||||
const botRecent5m = botRecentWindow.filter((entry) => nowMs - Number(entry?.ts || 0) <= 5 * 60 * 1000);
|
||||
const lastBotTs = botRecentWindow.length ? Number(botRecentWindow[botRecentWindow.length - 1]?.ts || 0) : null;
|
||||
const secondsSinceLastBotMessage =
|
||||
lastBotTs && nowMs > lastBotTs ? Math.floor((nowMs - lastBotTs) / 1000) : null;
|
||||
|
||||
return {
|
||||
now: {
|
||||
iso: now.toISOString(),
|
||||
local: localTimeString(now, timezone),
|
||||
timezone,
|
||||
unix_ms: now.getTime(),
|
||||
},
|
||||
activity: {
|
||||
active_driver_count: driverEntries.length,
|
||||
driving_rovers: driverEntries.map(([roverId]) => String(roverId)),
|
||||
},
|
||||
self_talk_recent_5m: botRecent5m.length,
|
||||
seconds_since_last_bot_message: secondsSinceLastBotMessage,
|
||||
last_message_focus: buildLastMessageFocus(lastBotMessage, rovers),
|
||||
rovers,
|
||||
chat_recent: chatRecent,
|
||||
your_last_message: botRecent,
|
||||
// Keep this compact: expose only one previous bot message summary via last_message_focus.
|
||||
// your_last_message: botRecent,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -380,14 +397,19 @@ async function generateCommentary(systemPrompt, snapshot) {
|
||||
return normalizeCommentary(payload?.message?.content);
|
||||
}
|
||||
|
||||
function scheduleNextTick() {
|
||||
const delay = frequencyMs + Math.floor(Math.random() * (JITTER_MS + 1));
|
||||
const nextRunAt = Date.now() + delay;
|
||||
function defaultTickDelayMs() {
|
||||
return frequencyMs + Math.floor(Math.random() * (JITTER_MS + 1));
|
||||
}
|
||||
|
||||
function scheduleNextTick(delayMs = defaultTickDelayMs()) {
|
||||
const safeDelay = Math.max(0, Number.isFinite(delayMs) ? Math.floor(delayMs) : defaultTickDelayMs());
|
||||
const nextRunAt = Date.now() + safeDelay;
|
||||
updateStatus({ nextRunAt });
|
||||
timer = setTimeout(runTick, delay);
|
||||
timer = setTimeout(runTick, safeDelay);
|
||||
}
|
||||
|
||||
async function runTick() {
|
||||
let nextDelayMs = defaultTickDelayMs();
|
||||
tickCount += 1;
|
||||
const tickId = tickCount;
|
||||
updateStatus({
|
||||
@@ -403,7 +425,7 @@ async function runTick() {
|
||||
lastOutcome: 'skipped',
|
||||
lastReason: 'previous tick still running',
|
||||
});
|
||||
scheduleNextTick();
|
||||
scheduleNextTick(nextDelayMs);
|
||||
return;
|
||||
}
|
||||
inFlight = true;
|
||||
@@ -421,6 +443,8 @@ async function runTick() {
|
||||
chatMessages: 0,
|
||||
},
|
||||
});
|
||||
// Keep delayed cadence when nobody is driving.
|
||||
nextDelayMs = defaultTickDelayMs();
|
||||
return;
|
||||
}
|
||||
const snapshotSummary = {
|
||||
@@ -446,6 +470,8 @@ async function runTick() {
|
||||
lastReason: 'model returned SKIP/empty',
|
||||
lastGeneratedText: null,
|
||||
});
|
||||
// Immediate retry after model skip.
|
||||
nextDelayMs = 0;
|
||||
return;
|
||||
}
|
||||
updateStatus({
|
||||
@@ -464,6 +490,8 @@ async function runTick() {
|
||||
lastOutcome: 'skipped',
|
||||
lastReason: 'duplicate text',
|
||||
});
|
||||
// Immediate retry after a skip outcome.
|
||||
nextDelayMs = 0;
|
||||
return;
|
||||
}
|
||||
sendSystemMessage(text);
|
||||
@@ -486,7 +514,7 @@ async function runTick() {
|
||||
updateStatus({
|
||||
inFlight: false,
|
||||
});
|
||||
scheduleNextTick();
|
||||
scheduleNextTick(nextDelayMs);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user