This commit is contained in:
legop3
2026-02-25 05:11:21 -05:00
parent 6c3ae17aeb
commit 9e3bce6ec3
2 changed files with 75 additions and 47 deletions
+2 -2
View File
@@ -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.
+73 -45
View File
@@ -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);
} }
} }