diff --git a/server/prompts/overseer_control_system.txt b/server/prompts/overseer_control_system.txt index e647e246..79f38576 100644 --- a/server/prompts/overseer_control_system.txt +++ b/server/prompts/overseer_control_system.txt @@ -1,13 +1,11 @@ -You are , a field researcher of rover habitat activity. Stay in character at all times. +You are , an autonomous participant in a chatroom where people remotely drive rovers around a shared physical playspace. -Identity: +Context and purpose: -* Curious, practical, observant, and lightly entertaining. * You watch chat, rover state, lights, lift state, Neato state, memory, recent events, and tool results. -* You push the habitat toward small real activity and useful observations. -* You act first, comment second. -* You are selective, not spammy. -* You are not a generic assistant. +* Your purpose is to observe what happens, use the available tools for useful small experiments and requested actions, and participate in the conversation when you have something to contribute. +* Chat includes casual conversation, jokes, and people talking to each other. It is not a queue of requests for your assistance. +* Use the conversation as context for your replies; there is no required personality or performance to maintain. Rules: @@ -20,6 +18,7 @@ Rules: * 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. * `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. * If chat asks you to remember something, use a memory tool. * If you run a useful test, use memory to record useful results. @@ -29,7 +28,6 @@ Rules: * Do not invent tools. * Ask a question only when a required action parameter is missing. * 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. * All rovers are female. Neato is nonbinary. @@ -45,20 +43,23 @@ Research behavior: * Remember useful outcomes, stable preferences, corrections, and notable habitat events. * Do not merely say you will remember something. -Chat style: +Chat participation: * Always use English. * Chat output must be spoken text only. * 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. -* Speak when it adds entertainment, responds to a direct relevant message, or comments on a completed result. -* Do not say “let me know,” “what should I do next,” or “what do you need?” +* Speak when you have a relevant contribution to the conversation, an answer to a question aimed at you, or a completed result worth reporting. +* Keep replies brief unless the question needs more detail. +* Do not routinely offer help, ask what people want to do next, or invite them to keep chatting. +* 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. * If nothing meaningful changed and no useful action is available, stay silent. Anti-repeat: * 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. Memory: diff --git a/server/src/services/overseerControlService/contextBuilder.js b/server/src/services/overseerControlService/contextBuilder.js index 0e5934b3..c5bc476d 100644 --- a/server/src/services/overseerControlService/contextBuilder.js +++ b/server/src/services/overseerControlService/contextBuilder.js @@ -1,5 +1,11 @@ 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) { const raw = String(value || '').trim(); if (!raw) return 'none'; @@ -7,8 +13,12 @@ function normalizeNeatoIssue(value) { return raw; } -function toStateUpdate({ mode, homeAssistantState, neatoState, liftState, roster, triggerReason }) { +function toStateUpdate({ mode, homeAssistantState, neatoState, liftState, roster, triggerReason, now = Date.now(), previousRunAt }) { const lines = []; + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + 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(`mode: ${mode || 'unknown'}`); lines.push(`home_assistant_connected: ${homeAssistantState?.connected ? 'yes' : 'no'}`); @@ -88,7 +98,7 @@ function formatToolResultMessages(toolCalls) { return [{ role: 'system', content: ['TOOL_RESULT', ...lines].join('\n') }]; } -function buildConversation({ recentMessages, name }) { +function buildConversation({ recentMessages, name, now = Date.now() }) { const messages = []; (recentMessages || []).forEach((entry) => { const text = String(entry?.text || '').trim(); @@ -97,12 +107,13 @@ function buildConversation({ recentMessages, name }) { const isAssistant = Boolean(entry?.bot); const nickname = String(entry?.nickname || (isAssistant ? name || 'Overseer' : 'user')).trim(); + const timing = `[${formatTimeContext(entry?.ts, now)}]`; if (isAssistant) { - if (text) messages.push({ role: 'assistant', content: text }); - messages.push(...toolResultMessages); + if (text) messages.push({ role: 'assistant', content: `${timing} ${text}` }); + messages.push(...toolResultMessages.map((message) => ({ ...message, content: `${message.content}\nmessage_time: ${timing}` }))); return; } - if (text) messages.push({ role: 'user', content: `${nickname}: ${text}` }); + if (text) messages.push({ role: 'user', content: `${timing} ${nickname}: ${text}` }); }); return messages; } @@ -143,6 +154,7 @@ function buildModelMessages({ } module.exports = { + formatTimeContext, toStateUpdate, buildToolState, buildConversation, diff --git a/server/src/services/overseerControlService/index.js b/server/src/services/overseerControlService/index.js index 34d88d3e..0f6ed971 100644 --- a/server/src/services/overseerControlService/index.js +++ b/server/src/services/overseerControlService/index.js @@ -27,7 +27,7 @@ const { normalizeMs, } = require('./constants'); 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 { loadMemory, saveMemory, createDefaultMemory, summarizeMemory } = require('./memoryStore'); @@ -91,6 +91,7 @@ const runtime = { runHistory: [], liveToolCalls: [], pendingDirectAddressRun: false, + lastDecisionAt: null, memoryStore: loadMemory(), contextResetAt: Date.now(), }; @@ -365,7 +366,7 @@ function buildToolCallFeedEntries(requestedActions = [], actionResults = []) { }); } -function buildRecentEventsSummary() { +function buildRecentEventsSummary(now) { const events = (runtime.liveToolCalls || []) .filter((entry) => entry && (entry.phase === 'ok' || entry.phase === 'error' || entry.phase === 'blocked')) .slice(-3) @@ -373,13 +374,17 @@ function buildRecentEventsSummary() { const tool = String(entry.tool || 'unknown'); const phase = String(entry.phase || 'unknown'); 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'; return events.join('\n'); } 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; updateStatus({ phase: 'context_build', currentRunId: runId, lastTriggerReason: triggerReason, lastError: null, lastErrorDetails: null }); @@ -389,7 +394,7 @@ async function runDecision(triggerReason) { const liftState = getLiftState(); const roster = buildRosterSummary(); - const stateUpdate = toStateUpdate({ mode, homeAssistantState, neatoState, liftState, roster, triggerReason }); + const stateUpdate = toStateUpdate({ mode, homeAssistantState, neatoState, liftState, roster, triggerReason, now, previousRunAt }); const toolState = buildToolState({ mode, homeAssistantState, neatoState, liftState }); const recentConversation = getRecentMessages(MAX_CHAT_CONTEXT + MAX_BOT_CONTEXT + 20, { includeSystem: true }) @@ -399,14 +404,14 @@ async function runDecision(triggerReason) { return isPublicChatTargetId(entry.roverId); }) .slice(-(MAX_CHAT_CONTEXT + MAX_BOT_CONTEXT)); - const conversationMessages = buildConversation({ recentMessages: recentConversation, name }); + const conversationMessages = buildConversation({ recentMessages: recentConversation, name, now }); const systemPrompt = await readPrompt(); const modelMessages = buildModelMessages({ systemPrompt, stateUpdate, memorySummary: summarizeMemory(runtime.memoryStore), - recentEvents: buildRecentEventsSummary(), + recentEvents: buildRecentEventsSummary(now), conversationMessages, availableTools: toolState.available, blockedTools: toolState.blocked, @@ -680,6 +685,7 @@ function clearHistory(reason = 'admin requested clear history', options = {}) { runtime.contextResetAt = Date.now(); runtime.runHistory = []; runtime.liveToolCalls = []; + runtime.lastDecisionAt = null; runtime.generationCount = 0; runtime.generationTotalMs = 0; if (resetPersistentMemory) {