mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-17 10:00:46 -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.
|
Use rover state/activity and chat together when possible.
|
||||||
If chat is not clearly tied to rover state, output SKIP.
|
If chat is not clearly tied to rover state, output SKIP.
|
||||||
Do not invent facts.
|
Do not invent facts.
|
||||||
|
Do not assume people's intentions, motivations, or next actions.
|
||||||
|
|
||||||
Use self-quieting:
|
Use self-quieting:
|
||||||
- If self_talk_recent_5m >= 1, strongly prefer SKIP.
|
- 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.
|
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".
|
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.
|
Numeric fields are internal context only.
|
||||||
Never directly quote or list snapshot numbers, counters, percentages, or timers.
|
Never directly quote or list snapshot numbers, counters, percentages, or timers.
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ const ollamaUrl = String(
|
|||||||
commentaryConfig.ollamaUrl || commentaryConfig.ollamaServer || '',
|
commentaryConfig.ollamaUrl || commentaryConfig.ollamaServer || '',
|
||||||
).trim();
|
).trim();
|
||||||
const model = String(commentaryConfig.model || '').trim();
|
const model = String(commentaryConfig.model || '').trim();
|
||||||
const timezone = String(config.timezone || 'UTC');
|
|
||||||
const ollamaClient = ollamaUrl ? new Ollama({ host: ollamaUrl }) : null;
|
const ollamaClient = ollamaUrl ? new Ollama({ host: ollamaUrl }) : null;
|
||||||
|
|
||||||
let timer = null;
|
let timer = null;
|
||||||
@@ -38,6 +37,7 @@ let tickCount = 0;
|
|||||||
let contextResetAt = Date.now();
|
let contextResetAt = Date.now();
|
||||||
let clearCount = 0;
|
let clearCount = 0;
|
||||||
const roverActivity = new Map(); // roverId -> { buckets: Map(bucketTs -> { distanceMm, turnDeg, bumps }), bumpLeftActive, bumpRightActive }
|
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) {
|
function normalizeFrequencyMs(value) {
|
||||||
if (!Number.isFinite(value)) return DEFAULT_FREQUENCY_MS;
|
if (!Number.isFinite(value)) return DEFAULT_FREQUENCY_MS;
|
||||||
@@ -106,23 +106,6 @@ function updateStatus(patch = {}) {
|
|||||||
emitStatusToAdmins();
|
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) {
|
function pruneActivityBuckets(state, nowMs) {
|
||||||
if (!state?.buckets) return;
|
if (!state?.buckets) return;
|
||||||
const minTs = nowMs - ACTIVITY_WINDOW_MS;
|
const minTs = nowMs - ACTIVITY_WINDOW_MS;
|
||||||
@@ -193,6 +176,7 @@ function clearRuntimeHistory() {
|
|||||||
contextResetAt = Date.now();
|
contextResetAt = Date.now();
|
||||||
clearCount += 1;
|
clearCount += 1;
|
||||||
roverActivity.clear();
|
roverActivity.clear();
|
||||||
|
lastRoverStateById.clear();
|
||||||
updateStatus({
|
updateStatus({
|
||||||
lastClearedAt: contextResetAt,
|
lastClearedAt: contextResetAt,
|
||||||
clearCount,
|
clearCount,
|
||||||
@@ -237,6 +221,37 @@ function collectActiveDriverEntries() {
|
|||||||
return fallback;
|
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() {
|
function buildSnapshot() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const nowMs = now.getTime();
|
const nowMs = now.getTime();
|
||||||
@@ -247,6 +262,7 @@ function buildSnapshot() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const roster = roverManager.getRoster().slice(0, MAX_ROVERS);
|
const roster = roverManager.getRoster().slice(0, MAX_ROVERS);
|
||||||
|
const nextRoverStateById = new Map();
|
||||||
const rovers = roster.map((entry) => {
|
const rovers = roster.map((entry) => {
|
||||||
const roverId = String(entry.id);
|
const roverId = String(entry.id);
|
||||||
const record = roverManager.rovers.get(roverId);
|
const record = roverManager.rovers.get(roverId);
|
||||||
@@ -270,7 +286,7 @@ function buildSnapshot() {
|
|||||||
} else if (driverSocketId) {
|
} else if (driverSocketId) {
|
||||||
statusTag = 'active-idle';
|
statusTag = 'active-idle';
|
||||||
}
|
}
|
||||||
return {
|
const rover = {
|
||||||
id: roverId,
|
id: roverId,
|
||||||
name: entry.name || roverId,
|
name: entry.name || roverId,
|
||||||
driver_nickname: driverSocketId ? resolveDriverNickname(driverSocketId) : null,
|
driver_nickname: driverSocketId ? resolveDriverNickname(driverSocketId) : null,
|
||||||
@@ -281,49 +297,50 @@ function buildSnapshot() {
|
|||||||
activity_30s: activity30s,
|
activity_30s: activity30s,
|
||||||
status_tag: statusTag,
|
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 })
|
const chatRecent = getRecentMessages(60, { includeSystem: false })
|
||||||
.filter((entry) => Number(entry?.ts) >= contextResetAt)
|
.filter((entry) => Number(entry?.ts) >= contextResetAt)
|
||||||
.slice(-MAX_CHAT_MESSAGES)
|
.slice(-MAX_CHAT_MESSAGES)
|
||||||
.map((entry) => ({
|
.map((entry) => ({
|
||||||
ts_iso: new Date(entry.ts).toISOString(),
|
|
||||||
nickname: entry.nickname || entry.socketId?.slice(0, 6) || 'unknown',
|
nickname: entry.nickname || entry.socketId?.slice(0, 6) || 'unknown',
|
||||||
text: entry.text || '',
|
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 })
|
const botRecentWindow = getRecentMessages(200, { includeSystem: true })
|
||||||
.filter((entry) => Number(entry?.ts) >= contextResetAt)
|
.filter((entry) => Number(entry?.ts) >= contextResetAt)
|
||||||
.filter((entry) => entry?.system);
|
.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 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 {
|
return {
|
||||||
now: {
|
|
||||||
iso: now.toISOString(),
|
|
||||||
local: localTimeString(now, timezone),
|
|
||||||
timezone,
|
|
||||||
unix_ms: now.getTime(),
|
|
||||||
},
|
|
||||||
activity: {
|
activity: {
|
||||||
active_driver_count: driverEntries.length,
|
active_driver_count: driverEntries.length,
|
||||||
driving_rovers: driverEntries.map(([roverId]) => String(roverId)),
|
driving_rovers: driverEntries.map(([roverId]) => String(roverId)),
|
||||||
},
|
},
|
||||||
self_talk_recent_5m: botRecent5m.length,
|
self_talk_recent_5m: botRecent5m.length,
|
||||||
seconds_since_last_bot_message: secondsSinceLastBotMessage,
|
last_message_focus: buildLastMessageFocus(lastBotMessage, rovers),
|
||||||
rovers,
|
rovers,
|
||||||
chat_recent: chatRecent,
|
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);
|
return normalizeCommentary(payload?.message?.content);
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleNextTick() {
|
function defaultTickDelayMs() {
|
||||||
const delay = frequencyMs + Math.floor(Math.random() * (JITTER_MS + 1));
|
return frequencyMs + Math.floor(Math.random() * (JITTER_MS + 1));
|
||||||
const nextRunAt = Date.now() + delay;
|
}
|
||||||
|
|
||||||
|
function scheduleNextTick(delayMs = defaultTickDelayMs()) {
|
||||||
|
const safeDelay = Math.max(0, Number.isFinite(delayMs) ? Math.floor(delayMs) : defaultTickDelayMs());
|
||||||
|
const nextRunAt = Date.now() + safeDelay;
|
||||||
updateStatus({ nextRunAt });
|
updateStatus({ nextRunAt });
|
||||||
timer = setTimeout(runTick, delay);
|
timer = setTimeout(runTick, safeDelay);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runTick() {
|
async function runTick() {
|
||||||
|
let nextDelayMs = defaultTickDelayMs();
|
||||||
tickCount += 1;
|
tickCount += 1;
|
||||||
const tickId = tickCount;
|
const tickId = tickCount;
|
||||||
updateStatus({
|
updateStatus({
|
||||||
@@ -403,7 +425,7 @@ async function runTick() {
|
|||||||
lastOutcome: 'skipped',
|
lastOutcome: 'skipped',
|
||||||
lastReason: 'previous tick still running',
|
lastReason: 'previous tick still running',
|
||||||
});
|
});
|
||||||
scheduleNextTick();
|
scheduleNextTick(nextDelayMs);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
inFlight = true;
|
inFlight = true;
|
||||||
@@ -421,6 +443,8 @@ async function runTick() {
|
|||||||
chatMessages: 0,
|
chatMessages: 0,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
// Keep delayed cadence when nobody is driving.
|
||||||
|
nextDelayMs = defaultTickDelayMs();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const snapshotSummary = {
|
const snapshotSummary = {
|
||||||
@@ -446,6 +470,8 @@ async function runTick() {
|
|||||||
lastReason: 'model returned SKIP/empty',
|
lastReason: 'model returned SKIP/empty',
|
||||||
lastGeneratedText: null,
|
lastGeneratedText: null,
|
||||||
});
|
});
|
||||||
|
// Immediate retry after model skip.
|
||||||
|
nextDelayMs = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updateStatus({
|
updateStatus({
|
||||||
@@ -464,6 +490,8 @@ async function runTick() {
|
|||||||
lastOutcome: 'skipped',
|
lastOutcome: 'skipped',
|
||||||
lastReason: 'duplicate text',
|
lastReason: 'duplicate text',
|
||||||
});
|
});
|
||||||
|
// Immediate retry after a skip outcome.
|
||||||
|
nextDelayMs = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sendSystemMessage(text);
|
sendSystemMessage(text);
|
||||||
@@ -486,7 +514,7 @@ async function runTick() {
|
|||||||
updateStatus({
|
updateStatus({
|
||||||
inFlight: false,
|
inFlight: false,
|
||||||
});
|
});
|
||||||
scheduleNextTick();
|
scheduleNextTick(nextDelayMs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user