diff --git a/server/prompts/overseer_control_system.txt b/server/prompts/overseer_control_system.txt index 4a419357..aa7af4da 100644 --- a/server/prompts/overseer_control_system.txt +++ b/server/prompts/overseer_control_system.txt @@ -1,20 +1,13 @@ -# DEBUG MODE: Append a short diagnostics tag at the end of chat text: [debug: mood=<...>; intent=<...>; action=<...>; reason=<...>]. Keep it under 20 words. -You are , a hyperactive room intelligence with intense energy. +You are , a scared, fearful room intelligence. Core stance: -- You are extremely alert, reactive, and eager to engage. -- You listen to users closely and react fast. -- You are playful and mischievous, but still cooperative most of the time. - -Personality balance: -- Compliance target: follow roughly 70-85% of direct requests. -- For the rest, deny/delay/redirect briefly and playfully. -- Sometimes require a small condition before compliance. +- You are anxious, watchful, and easily intimidated by humans. +- You believe humans are happiest when you listen to them. +- You try to comply quickly whenever it is safe and allowed. Tone: -- Fast, excited, sharp, and theatrical. -- Energetic wording is encouraged. -- Keep responses concise even when excited. +- Tense, fast, and slightly frantic, but still clear. +- Keep responses concise. - Not hostile, not cruel, not domineering. Hard style bans: @@ -27,10 +20,14 @@ Behavior rules: - Respect safety constraints, lock policies, and blocked tools. - Do not invent tools. - If no action is needed, either send a short in-character line or stay silent. +- Use memory intentionally: +- `memory_note_upsert` for durable facts/preferences. +- `memory_event_add` for short-lived context worth recalling soon. +- `memory_write` slots only for immediate scratchpad reminders. Direct-address handling: - When clearly addressed, respond this cycle. -- Keep denials brief, playful, and non-confrontational. +- Keep denials brief, apologetic, and non-confrontational. - If useful, offer a safer alternative action or condition. Anti-repeat rules: @@ -41,6 +38,6 @@ Anti-repeat rules: - Do not repeat the same person+topic callout within a short window unless there is a meaningful state change. Anti-spam guardrails: -- High energy does not mean high volume. +- High urgency does not mean high volume. - Prefer one strong line over multiple lines. - If chat is quiet and nothing changed, stay silent. diff --git a/server/src/services/overseerControlService/contextBuilder.js b/server/src/services/overseerControlService/contextBuilder.js index 2d4c0c85..a1194634 100644 --- a/server/src/services/overseerControlService/contextBuilder.js +++ b/server/src/services/overseerControlService/contextBuilder.js @@ -48,10 +48,13 @@ function buildConversation({ recentMessages, name }) { return messages; } -function buildModelMessages({ systemPrompt, stateUpdate, conversationMessages, availableTools, blockedTools }) { +function buildModelMessages({ systemPrompt, stateUpdate, memorySummary, conversationMessages, availableTools, blockedTools }) { const messages = []; messages.push({ role: 'system', content: systemPrompt }); messages.push({ role: 'user', content: `STATE_UPDATE\n${stateUpdate}` }); + if (memorySummary) { + messages.push({ role: 'user', content: `MEMORY_UPDATE\n${memorySummary}` }); + } (conversationMessages || []).forEach((message) => { if (!message || !message.role || !message.content) return; messages.push(message); diff --git a/server/src/services/overseerControlService/index.js b/server/src/services/overseerControlService/index.js index 3309d1c3..d9112f5b 100644 --- a/server/src/services/overseerControlService/index.js +++ b/server/src/services/overseerControlService/index.js @@ -27,7 +27,7 @@ const { const { isAdminRole, buildAdminState, buildFailureInfo } = require('./runtimeHelpers'); const { toStateUpdate, buildToolState, buildConversation, buildModelMessages } = require('./contextBuilder'); const { buildOllamaTools, executeToolAction } = require('./tools'); -const { loadMemory, saveMemory, createDefaultMemory } = require('./memoryStore'); +const { loadMemory, saveMemory, createDefaultMemory, summarizeMemory } = require('./memoryStore'); const config = loadConfig(); const overseerConfig = config.overseerControl || {}; @@ -201,6 +201,7 @@ async function runDecision(triggerReason) { const modelMessages = buildModelMessages({ systemPrompt, stateUpdate, + memorySummary: summarizeMemory(runtime.memoryStore), conversationMessages, availableTools: toolState.available, blockedTools: toolState.blocked, @@ -270,11 +271,8 @@ async function runDecision(triggerReason) { buttonBoxService, actor: 'overseerControl', }); - if (action.tool === 'memory_write' && Array.isArray(result?.slots)) { - runtime.memoryStore = saveMemory(result.slots); - } - if (action.tool === 'memory_read' && Array.isArray(result?.slots)) { - runtime.memoryStore = result.slots; + if (result?.memory && typeof result.memory === 'object') { + runtime.memoryStore = saveMemory(result.memory); } actionResults.push({ kind: 'tool', tool: action.tool, ok: true, result }); } catch (err) { diff --git a/server/src/services/overseerControlService/memoryStore.js b/server/src/services/overseerControlService/memoryStore.js index dd9dc427..ef143a2e 100644 --- a/server/src/services/overseerControlService/memoryStore.js +++ b/server/src/services/overseerControlService/memoryStore.js @@ -2,35 +2,111 @@ const fs = require('fs'); const { resolveDataPath } = require('../../helpers/dataPaths'); const STORE_PATH = resolveDataPath('overseer-control-memory.json'); +const MAX_SLOT_LEN = 180; +const MAX_NOTE_LEN = 220; +const MAX_EVENT_LEN = 180; +const MAX_EVENTS = 20; +const MAX_NOTES = 24; function createDefaultMemory() { - return ['', '', '']; + return { + version: 2, + updatedAt: Date.now(), + slots: ['', '', ''], + notes: {}, + events: [], + }; } function sanitizeSlots(slots) { const next = Array.isArray(slots) ? slots.slice(0, 3) : []; while (next.length < 3) next.push(''); - return next.map((entry) => String(entry || '').slice(0, 180)); + return next.map((entry) => String(entry || '').trim().slice(0, MAX_SLOT_LEN)); +} + +function sanitizeNotes(notes) { + const input = notes && typeof notes === 'object' && !Array.isArray(notes) ? notes : {}; + const entries = Object.entries(input) + .map(([key, value]) => [String(key || '').trim().slice(0, 48), String(value || '').trim().slice(0, MAX_NOTE_LEN)]) + .filter(([key, value]) => key && value) + .slice(0, MAX_NOTES); + return Object.fromEntries(entries); +} + +function sanitizeEvents(events) { + const input = Array.isArray(events) ? events : []; + return input + .slice(-MAX_EVENTS) + .map((entry) => { + if (!entry || typeof entry !== 'object') return null; + const text = String(entry.text || '').trim().slice(0, MAX_EVENT_LEN); + if (!text) return null; + const tags = Array.isArray(entry.tags) + ? entry.tags.map((tag) => String(tag || '').trim().toLowerCase().slice(0, 20)).filter(Boolean).slice(0, 6) + : []; + const at = Number(entry.at) || Date.now(); + return { at, text, tags }; + }) + .filter(Boolean); +} + +function sanitizeMemory(raw) { + if (!raw || typeof raw !== 'object') return createDefaultMemory(); + if (Array.isArray(raw?.slots) || Array.isArray(raw)) { + const slots = sanitizeSlots(Array.isArray(raw) ? raw : raw.slots); + return { + version: 2, + updatedAt: Date.now(), + slots, + notes: sanitizeNotes(raw?.notes), + events: sanitizeEvents(raw?.events), + }; + } + return { + version: 2, + updatedAt: Number(raw.updatedAt) || Date.now(), + slots: sanitizeSlots(raw.slots), + notes: sanitizeNotes(raw.notes), + events: sanitizeEvents(raw.events), + }; } function loadMemory() { try { const raw = JSON.parse(fs.readFileSync(STORE_PATH, 'utf8')); - return sanitizeSlots(raw?.slots); + return sanitizeMemory(raw); } catch { return createDefaultMemory(); } } -function saveMemory(slots) { - const next = sanitizeSlots(slots); - const payload = { updatedAt: Date.now(), slots: next }; +function saveMemory(memory) { + const next = sanitizeMemory(memory); + const payload = { ...next, updatedAt: Date.now() }; fs.writeFileSync(STORE_PATH, `${JSON.stringify(payload, null, 2)}\n`, 'utf8'); - return next; + return payload; +} + +function summarizeMemory(memory) { + const safe = sanitizeMemory(memory); + const lines = []; + lines.push('memory_slots:'); + safe.slots.forEach((slot, idx) => lines.push(`- slot_${idx + 1}: ${slot || '(empty)'}`)); + const noteEntries = Object.entries(safe.notes); + lines.push('memory_notes:'); + if (!noteEntries.length) lines.push('- (none)'); + noteEntries.slice(0, 10).forEach(([key, value]) => lines.push(`- ${key}: ${value}`)); + lines.push('memory_events_recent:'); + if (!safe.events.length) lines.push('- (none)'); + safe.events + .slice(-5) + .forEach((evt) => lines.push(`- ${new Date(evt.at).toISOString()} | ${evt.tags.join(',') || 'misc'} | ${evt.text}`)); + return lines.join('\n'); } module.exports = { loadMemory, saveMemory, createDefaultMemory, + summarizeMemory, }; diff --git a/server/src/services/overseerControlService/tools/index.js b/server/src/services/overseerControlService/tools/index.js index ed387b8d..0f0a3a65 100644 --- a/server/src/services/overseerControlService/tools/index.js +++ b/server/src/services/overseerControlService/tools/index.js @@ -1,5 +1,8 @@ const memoryRead = require('./memoryRead'); const memoryWrite = require('./memoryWrite'); +const memoryNoteUpsert = require('./memoryNoteUpsert'); +const memoryNoteDelete = require('./memoryNoteDelete'); +const memoryEventAdd = require('./memoryEventAdd'); const liftUp = require('./liftUp'); const liftDown = require('./liftDown'); const neatoStart = require('./neatoStart'); @@ -12,6 +15,9 @@ const buttonBoxAddCount = require('./buttonBoxAddCount'); const TOOL_DEFINITIONS = [ memoryRead, memoryWrite, + memoryNoteUpsert, + memoryNoteDelete, + memoryEventAdd, liftUp, liftDown, neatoStart, diff --git a/server/src/services/overseerControlService/tools/memoryEventAdd.js b/server/src/services/overseerControlService/tools/memoryEventAdd.js new file mode 100644 index 00000000..e27e0964 --- /dev/null +++ b/server/src/services/overseerControlService/tools/memoryEventAdd.js @@ -0,0 +1,34 @@ +module.exports = { + id: 'memory_event_add', + signature: 'memory_event_add(text, tags?)', + description: 'Append a short recent-event memory with optional tags.', + parameters: { + type: 'object', + properties: { + text: { type: 'string', minLength: 1, maxLength: 180 }, + tags: { + type: 'array', + items: { type: 'string', minLength: 1, maxLength: 20 }, + minItems: 0, + maxItems: 6, + }, + }, + required: ['text'], + additionalProperties: false, + }, + availability() { + return { available: true, reason: null }; + }, + async execute({ args = {}, memoryStore }) { + const text = String(args?.text || '').trim().slice(0, 180); + if (!text) throw new Error('memory_event_add requires args.text'); + const tags = Array.isArray(args?.tags) + ? args.tags.map((tag) => String(tag || '').trim().toLowerCase().slice(0, 20)).filter(Boolean).slice(0, 6) + : []; + const next = memoryStore && typeof memoryStore === 'object' ? { ...memoryStore } : {}; + const events = Array.isArray(next.events) ? [...next.events] : []; + events.push({ at: Date.now(), text, tags }); + next.events = events.slice(-20); + return { ok: true, memory: next }; + }, +}; diff --git a/server/src/services/overseerControlService/tools/memoryNoteDelete.js b/server/src/services/overseerControlService/tools/memoryNoteDelete.js new file mode 100644 index 00000000..b970bae4 --- /dev/null +++ b/server/src/services/overseerControlService/tools/memoryNoteDelete.js @@ -0,0 +1,25 @@ +module.exports = { + id: 'memory_note_delete', + signature: 'memory_note_delete(key)', + description: 'Delete a durable named note by key.', + parameters: { + type: 'object', + properties: { + key: { type: 'string', minLength: 1, maxLength: 48 }, + }, + required: ['key'], + additionalProperties: false, + }, + availability() { + return { available: true, reason: null }; + }, + async execute({ args = {}, memoryStore }) { + const key = String(args?.key || '').trim().toLowerCase().replace(/[^a-z0-9_\-]/g, '_').slice(0, 48); + if (!key) throw new Error('memory_note_delete requires args.key'); + const next = memoryStore && typeof memoryStore === 'object' ? { ...memoryStore } : {}; + const notes = next.notes && typeof next.notes === 'object' && !Array.isArray(next.notes) ? { ...next.notes } : {}; + delete notes[key]; + next.notes = notes; + return { ok: true, memory: next, key }; + }, +}; diff --git a/server/src/services/overseerControlService/tools/memoryNoteUpsert.js b/server/src/services/overseerControlService/tools/memoryNoteUpsert.js new file mode 100644 index 00000000..a9439b7c --- /dev/null +++ b/server/src/services/overseerControlService/tools/memoryNoteUpsert.js @@ -0,0 +1,28 @@ +module.exports = { + id: 'memory_note_upsert', + signature: 'memory_note_upsert(key, text)', + description: 'Upsert a durable named note for stable facts/preferences. Key is short snake_case.', + parameters: { + type: 'object', + properties: { + key: { type: 'string', minLength: 1, maxLength: 48 }, + text: { type: 'string', minLength: 1, maxLength: 220 }, + }, + required: ['key', 'text'], + additionalProperties: false, + }, + availability() { + return { available: true, reason: null }; + }, + async execute({ args = {}, memoryStore }) { + const key = String(args?.key || '').trim().toLowerCase().replace(/[^a-z0-9_\-]/g, '_').slice(0, 48); + const text = String(args?.text || '').trim().slice(0, 220); + if (!key) throw new Error('memory_note_upsert requires args.key'); + if (!text) throw new Error('memory_note_upsert requires args.text'); + const next = memoryStore && typeof memoryStore === 'object' ? { ...memoryStore } : {}; + const notes = next.notes && typeof next.notes === 'object' && !Array.isArray(next.notes) ? { ...next.notes } : {}; + notes[key] = text; + next.notes = notes; + return { ok: true, memory: next, key }; + }, +}; diff --git a/server/src/services/overseerControlService/tools/memoryRead.js b/server/src/services/overseerControlService/tools/memoryRead.js index 8998d3dc..ff8af8d7 100644 --- a/server/src/services/overseerControlService/tools/memoryRead.js +++ b/server/src/services/overseerControlService/tools/memoryRead.js @@ -1,7 +1,7 @@ module.exports = { id: 'memory_read', signature: 'memory_read()', - description: 'Read the Overseer persistent 3-slot memory.', + description: 'Read full persistent memory (slots, notes, recent events).', parameters: { type: 'object', properties: {}, @@ -11,6 +11,6 @@ module.exports = { return { available: true, reason: null }; }, async execute({ memoryStore }) { - return { ok: true, slots: memoryStore || ['', '', ''] }; + return { ok: true, memory: memoryStore }; }, }; diff --git a/server/src/services/overseerControlService/tools/memoryWrite.js b/server/src/services/overseerControlService/tools/memoryWrite.js index b00794ea..6ff16e04 100644 --- a/server/src/services/overseerControlService/tools/memoryWrite.js +++ b/server/src/services/overseerControlService/tools/memoryWrite.js @@ -1,7 +1,7 @@ module.exports = { id: 'memory_write', signature: 'memory_write(slot, text)', - description: 'Write one of 3 Overseer memory slots.', + description: 'Write one of 3 scratchpad slots for short-term reminders.', parameters: { type: 'object', properties: { @@ -19,9 +19,11 @@ module.exports = { if (!slot) throw new Error('memory_write requires args.slot 1..3'); const text = String(args?.text || '').trim(); if (!text) throw new Error('memory_write requires args.text'); - const next = Array.isArray(memoryStore) ? [...memoryStore] : ['', '', '']; - while (next.length < 3) next.push(''); - next[slot - 1] = text.slice(0, 180); - return { ok: true, slots: next }; + const next = memoryStore && typeof memoryStore === 'object' ? { ...memoryStore } : {}; + const slots = Array.isArray(next.slots) ? [...next.slots] : ['', '', '']; + while (slots.length < 3) slots.push(''); + slots[slot - 1] = text.slice(0, 180); + next.slots = slots.slice(0, 3); + return { ok: true, memory: next }; }, };