diff --git a/services/minecraft/src/cognitive/conscious/brain.ts b/services/minecraft/src/cognitive/conscious/brain.ts index 272f7217a..8a0dce1a4 100644 --- a/services/minecraft/src/cognitive/conscious/brain.ts +++ b/services/minecraft/src/cognitive/conscious/brain.ts @@ -813,11 +813,32 @@ export class Brain { return JSON.parse(JSON.stringify(messages)) as Message[] } + /** + * Re-emit the current conversation state with full context metadata. + * Used by the debug dashboard's `request_conversation` handler on reconnect. + */ + public broadcastConversationState(): void { + this.emitConversationUpdate(this.isProcessing) + } + private emitConversationUpdate(isProcessing: boolean, sessionBoundary?: boolean): void { this.debugService.emitConversationUpdate({ messages: this.cloneMessages(this.conversationHistory), isProcessing, ...(sessionBoundary && { sessionBoundary }), + activeContext: { + label: this.activeContextState.label, + startTurnId: this.activeContextState.startTurnId, + messageCount: this.conversationHistory.length - this.activeContextStartIndex, + }, + archivedContexts: this.archivedContexts.map(ctx => ({ + label: ctx.label, + summary: ctx.summary, + turns: ctx.endTurnId - ctx.startTurnId + 1, + archivedAt: ctx.archivedAt, + })), + activeContextStartIndex: this.activeContextStartIndex, + contextHistoryMessage: this.getContextHistoryMessage(), }) } diff --git a/services/minecraft/src/cognitive/index.ts b/services/minecraft/src/cognitive/index.ts index 43046fec4..e25698bcf 100644 --- a/services/minecraft/src/cognitive/index.ts +++ b/services/minecraft/src/cognitive/index.ts @@ -33,11 +33,7 @@ export function CognitiveEngine(options: CognitiveEngineOptions): MineflayerPlug }) debugService.onCommand('request_conversation', () => { - const snapshot = brain.getDebugSnapshot() - debugService.emitConversationUpdate({ - messages: snapshot.conversationHistory, - isProcessing: snapshot.isProcessing, - }) + brain.broadcastConversationState() }) debugService.onCommand('execute_repl', async (command) => { diff --git a/services/minecraft/src/debug/types.ts b/services/minecraft/src/debug/types.ts index b645d5809..6cc596447 100644 --- a/services/minecraft/src/debug/types.ts +++ b/services/minecraft/src/debug/types.ts @@ -96,6 +96,16 @@ export interface TraceBatchEvent { timestamp: number } +/** + * Archived context summary for the debug dashboard + */ +export interface ContextBoundaryInfo { + label: string + summary: string + turns: number + archivedAt: number +} + /** * Live conversation state update from the brain */ @@ -103,6 +113,18 @@ export interface ConversationUpdateEvent { messages: Array<{ role: string, content: string, reasoning?: string }> isProcessing: boolean sessionBoundary?: boolean + /** Active context boundary state */ + activeContext?: { + label: string | null + startTurnId: number + messageCount: number + } + /** Archived context summaries */ + archivedContexts?: ContextBoundaryInfo[] + /** Index in messages where the active context starts */ + activeContextStartIndex?: number + /** The [CONTEXT_HISTORY] prefix message content, if any */ + contextHistoryMessage?: string | null timestamp: number } diff --git a/services/minecraft/src/debug/web/app.js b/services/minecraft/src/debug/web/app.js index 68f54c00d..b61959728 100644 --- a/services/minecraft/src/debug/web/app.js +++ b/services/minecraft/src/debug/web/app.js @@ -566,13 +566,39 @@ class LogsPanel { // Conversation Panel (Live Chat View) // ============================================================================= +// --- User message parser: extracts structured sections from brain's buildUserMessage output --- +function parseUserMessage(content) { + if (typeof content !== 'string') + return { sections: [], raw: '' } + const sections = [] + // Known section tags in order they may appear + const tagPattern = /^\[(EVENT|FEEDBACK|PERCEPTION|STATE|ERROR_BURST_GUARD|ERROR_BURST|MANDATORY|SCRIPT|ACTION_QUEUE|NO_ACTION_BUDGET|CONTEXT)\]\s*/ + // Split on double-newline (the separator used by buildUserMessage) + const blocks = content.split(/\n\n/) + for (const block of blocks) { + const trimmed = block.trim() + if (!trimmed) + continue + const m = trimmed.match(tagPattern) + if (m) { + sections.push({ tag: m[1], text: trimmed.slice(m[0].length) }) + } + else { + // Could be a continuation of PERCEPTION or unknown block + sections.push({ tag: 'OTHER', text: trimmed }) + } + } + return { sections, raw: content } +} + class ConversationPanel { constructor(client) { this.client = client - // Each session: { messages: Message[], greyed: boolean } - this.sessions = [{ messages: [], greyed: false }] + this._mkSession = () => ({ messages: [], greyed: false, activeContext: null, archivedContexts: [], activeContextStartIndex: 0, contextHistoryMessage: null }) + this.sessions = [this._mkSession()] this.isProcessing = false this.autoScroll = true + this.turnCounter = 0 this.elements = { container: document.getElementById('conversation-container'), count: document.getElementById('conversation-count'), @@ -588,44 +614,63 @@ class ConversationPanel { this.reset() this.client.send({ type: 'request_conversation' }) }) + + // Unified collapsible toggle handler (event delegation — attached once) + if (this.elements.container) { + this.elements.container.addEventListener('click', (e) => { + const toggle = e.target.closest('[data-toggle]') + if (!toggle) + return + const targetId = toggle.getAttribute('data-toggle') + const body = document.getElementById(targetId) + if (!body) + return + const isOpen = body.classList.toggle('cv-open') + const arrow = toggle.querySelector('.cv-arrow') + if (arrow) + arrow.textContent = isOpen ? '\u25BC' : '\u25B6' + }) + } + this.render() } handleUpdate(data) { if (data.sessionBoundary) { - // Grey out current session and start a new one - const currentSession = this.sessions[this.sessions.length - 1] - if (currentSession) { - currentSession.greyed = true - } - this.sessions.push({ messages: [], greyed: false }) + const cur = this.sessions[this.sessions.length - 1] + if (cur) + cur.greyed = true + this.sessions.push(this._mkSession()) } else { - // Update current session messages - const currentSession = this.sessions[this.sessions.length - 1] - if (currentSession) { - currentSession.messages = data.messages || [] + const cur = this.sessions[this.sessions.length - 1] + if (cur) { + cur.messages = data.messages || [] + cur.activeContext = data.activeContext || null + cur.archivedContexts = data.archivedContexts || [] + cur.activeContextStartIndex = data.activeContextStartIndex ?? 0 + cur.contextHistoryMessage = data.contextHistoryMessage || null } } - this.isProcessing = !!data.isProcessing this.updateStats() this.render() } reset() { - this.sessions = [{ messages: [], greyed: false }] + this.sessions = [this._mkSession()] this.isProcessing = false + this.turnCounter = 0 this.updateStats() this.render() } updateStats() { - const totalMessages = this.sessions.reduce((sum, s) => sum + s.messages.length, 0) + const total = this.sessions.reduce((s, sess) => s + sess.messages.length, 0) if (this.elements.count) - this.elements.count.textContent = totalMessages + this.elements.count.textContent = total if (this.elements.statLlm) - this.elements.statLlm.textContent = totalMessages + this.elements.statLlm.textContent = total if (this.elements.processingBadge) this.elements.processingBadge.classList.toggle('hidden', !this.isProcessing) } @@ -633,100 +678,305 @@ class ConversationPanel { render() { if (!this.elements.container) return + this.turnCounter = 0 - const html = this.sessions.map((session, sessionIdx) => { - const sessionClass = session.greyed ? 'chat-session chat-session-greyed' : 'chat-session' - const messagesHtml = this.renderSessionMessages(session.messages) - const dividerHtml = session.greyed - ? '