From c46b3fa3d2aa95a1c53747b66612669bee3761cc Mon Sep 17 00:00:00 2001 From: Rin Date: Sun, 11 Jan 2026 23:47:06 +0800 Subject: [PATCH] feat(minecraft) debug server redesigned --- pnpm-lock.yaml | 19 +- pnpm-workspace.yaml | 2 + services/minecraft/package.json | 4 + .../src/cognitive/conscious/brain.ts | 2 +- .../src/cognitive/conscious/completion.ts | 2 +- .../src/cognitive/conscious/handler.ts | 2 +- .../src/cognitive/perception/pipeline.ts | 6 +- .../cognitive/perception/saliency-detector.ts | 224 +++--- .../cognitive/perception/saliency-rules.ts | 289 +++---- services/minecraft/src/debug-server.ts | 211 ------ services/minecraft/src/debug/debug-service.ts | 175 +++++ services/minecraft/src/debug/index.ts | 13 + services/minecraft/src/debug/server.ts | 334 ++++++++ services/minecraft/src/debug/types.ts | 119 +++ services/minecraft/src/debug/web/app.js | 654 ++++++++++++++++ services/minecraft/src/debug/web/index.html | 129 ++++ services/minecraft/src/debug/web/styles.css | 554 ++++++++++++++ services/minecraft/src/main.ts | 2 +- services/minecraft/src/utils/logger.ts | 2 +- services/minecraft/src/web/dashboard.html | 711 ------------------ 20 files changed, 2280 insertions(+), 1174 deletions(-) delete mode 100644 services/minecraft/src/debug-server.ts create mode 100644 services/minecraft/src/debug/debug-service.ts create mode 100644 services/minecraft/src/debug/index.ts create mode 100644 services/minecraft/src/debug/server.ts create mode 100644 services/minecraft/src/debug/types.ts create mode 100644 services/minecraft/src/debug/web/app.js create mode 100644 services/minecraft/src/debug/web/index.html create mode 100644 services/minecraft/src/debug/web/styles.css delete mode 100644 services/minecraft/src/web/dashboard.html diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b1f79eee..6c1558282 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,6 +69,9 @@ catalogs: '@types/unist': specifier: ^3.0.3 version: 3.0.3 + '@types/ws': + specifier: ^8.18.1 + version: 8.18.1 '@vueuse/core': specifier: 14.1.0 version: 14.1.0 @@ -201,6 +204,9 @@ catalogs: vue-sonner: specifier: 2.0.9 version: 2.0.9 + ws: + specifier: ^8.18.3 + version: 8.19.0 xsschema: specifier: 0.4.0-beta.13 version: 0.4.0-beta.13 @@ -3048,7 +3054,7 @@ importers: version: 14.1.0(vue@3.5.26(typescript@5.9.3)) '@wxt-dev/module-vue': specifier: ^1.0.3 - version: 1.0.3(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))(wxt@0.20.13(@types/node@24.10.9)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.55.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 1.0.3(vite@8.0.0-beta.9(@types/node@24.10.9)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))(wxt@0.20.13(@types/node@24.10.9)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.55.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) nanoid: specifier: ^5.1.6 version: 5.1.6 @@ -3187,12 +3193,19 @@ importers: vec3: specifier: ^0.1.10 version: 0.1.10 + ws: + specifier: 'catalog:' + version: 8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) zod: specifier: ^4.3.5 version: 4.3.5 zod-to-json-schema: specifier: ^3.25.1 version: 3.25.1(zod@4.3.5) + devDependencies: + '@types/ws': + specifier: 'catalog:' + version: 8.18.1 services/satori-bot: dependencies: @@ -23300,9 +23313,9 @@ snapshots: '@types/filesystem': 0.0.36 '@types/har-format': 1.2.16 - '@wxt-dev/module-vue@1.0.3(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))(wxt@0.20.13(@types/node@24.10.9)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.55.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': + '@wxt-dev/module-vue@1.0.3(vite@8.0.0-beta.9(@types/node@24.10.9)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))(wxt@0.20.13(@types/node@24.10.9)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.55.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@vitejs/plugin-vue': 6.0.3(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3)) + '@vitejs/plugin-vue': 6.0.3(vite@8.0.0-beta.9(@types/node@24.10.9)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3)) wxt: 0.20.13(@types/node@24.10.9)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.55.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - vite diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0f7439564..2dd8c4de1 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -46,6 +46,7 @@ catalog: '@types/hast': ^3.0.4 '@types/splitpanes': ^2.2.6 '@types/unist': ^3.0.3 + '@types/ws': ^8.18.1 '@vueuse/core': 14.1.0 '@xsai-ext/providers': ^0.4.2 '@xsai/embed': ^0.4.2 @@ -90,6 +91,7 @@ catalog: vite-plugin-mkcert: ^1.17.9 vue: ^3.5.25 vue-sonner: 2.0.9 + ws: ^8.18.3 xsschema: 0.4.0-beta.13 zod: ^4.3.5 diff --git a/services/minecraft/package.json b/services/minecraft/package.json index 6b70afe7c..1816de62a 100644 --- a/services/minecraft/package.json +++ b/services/minecraft/package.json @@ -36,6 +36,10 @@ "valibot": "^1.2.0", "vec3": "^0.1.10", "zod": "^4.3.5", + "ws": "catalog:", "zod-to-json-schema": "^3.25.1" + }, + "devDependencies": { + "@types/ws": "catalog:" } } diff --git a/services/minecraft/src/cognitive/conscious/brain.ts b/services/minecraft/src/cognitive/conscious/brain.ts index 3a2c1f34e..4a6449376 100644 --- a/services/minecraft/src/cognitive/conscious/brain.ts +++ b/services/minecraft/src/cognitive/conscious/brain.ts @@ -11,7 +11,7 @@ import type { BotEvent, MineflayerWithAgents } from '../types' import { system, user } from 'neuri/openai' import { config } from '../../composables/config' -import { DebugService } from '../../debug-server' +import { DebugService } from '../../debug' import { Blackboard } from './blackboard' import { buildConsciousContextView } from './context-view' import { generateBrainSystemPrompt } from './prompts/brain-prompt' diff --git a/services/minecraft/src/cognitive/conscious/completion.ts b/services/minecraft/src/cognitive/conscious/completion.ts index cbbb368da..e6ef0d791 100644 --- a/services/minecraft/src/cognitive/conscious/completion.ts +++ b/services/minecraft/src/cognitive/conscious/completion.ts @@ -7,7 +7,7 @@ import type { MineflayerWithAgents } from '../types' import { assistant } from 'neuri/openai' import { config } from '../../composables/config' -import { DebugService } from '../../debug-server' +import { DebugService } from '../../debug' export async function handleLLMCompletion(context: NeuriContext, bot: MineflayerWithAgents, logger: Logger): Promise { logger.log('rerouting...') diff --git a/services/minecraft/src/cognitive/conscious/handler.ts b/services/minecraft/src/cognitive/conscious/handler.ts index 00c4525ca..169b261f5 100644 --- a/services/minecraft/src/cognitive/conscious/handler.ts +++ b/services/minecraft/src/cognitive/conscious/handler.ts @@ -7,7 +7,7 @@ import type { LLMConfig, LLMResponse } from '../types' import { withRetry } from '@moeru/std' import { config } from '../../composables/config' -import { DebugService } from '../../debug-server' +import { DebugService } from '../../debug' import { useLogger } from '../../utils/logger' export abstract class BaseLLMHandler { diff --git a/services/minecraft/src/cognitive/perception/pipeline.ts b/services/minecraft/src/cognitive/perception/pipeline.ts index 656c1824b..66a112425 100644 --- a/services/minecraft/src/cognitive/perception/pipeline.ts +++ b/services/minecraft/src/cognitive/perception/pipeline.ts @@ -6,10 +6,10 @@ import type { PerceptionFrame } from './frame' import type { PerceptionSignal } from './types/signals' import type { PerceptionStage } from './types/stage' -import { DebugService } from '../../debug-server' -import { SaliencyDetector } from './saliency-detector' +import { DebugService } from '../../debug' import { createPerceptionFrameFromRawEvent } from './frame' import { MineflayerPerceptionCollector } from './mineflayer-perception-collector' +import { SaliencyDetector } from './saliency-detector' export class PerceptionPipeline { private readonly detector: SaliencyDetector @@ -119,7 +119,7 @@ export class PerceptionPipeline { this.saliencyEmitTimer = setInterval(() => { if (!this.initialized) return - DebugService.getInstance().emit('saliency', this.detector.getDebugSnapshot({ maxKeys: 30 })) + DebugService.getInstance().emit('saliency', this.detector.getDebugSnapshot()) }, 100) this.collector = new MineflayerPerceptionCollector({ diff --git a/services/minecraft/src/cognitive/perception/saliency-detector.ts b/services/minecraft/src/cognitive/perception/saliency-detector.ts index a2fbad56f..6eb5aedcf 100644 --- a/services/minecraft/src/cognitive/perception/saliency-detector.ts +++ b/services/minecraft/src/cognitive/perception/saliency-detector.ts @@ -1,41 +1,76 @@ import type { Logg } from '@guiiai/logg' -import { DEFAULT_THRESHOLD, DEFAULT_WINDOW_TICKS, SALIENCY_RULES, type SaliencyRuleBook } from './saliency-rules' +import type { SaliencyRuleBook } from './saliency-rules' import type { RawPerceptionEvent } from './types/raw-events' import type { PerceptionSignal } from './types/signals' -type WindowCounter = { - windowTicks: number +import { EVENT_KEYS, SALIENCY_RULES, WINDOW_SIZE } from './saliency-rules' + +/** + * Circular buffer counter for a single event type + */ +interface WindowCounter { + /** Current head position in circular buffer */ head: number + /** Event counts per slot */ counts: number[] + /** Trigger markers per slot (1 = fired in this slot) */ triggers: number[] + /** Running total of counts in window */ total: number + /** Slot when last event was received */ lastEventSlot: number + /** Slot when threshold was last triggered */ + lastFireSlot: number | null + /** Total count when last fired */ + lastFireTotal: number +} + +/** + * Debug snapshot data for a single counter + */ +export interface CounterSnapshot { + key: string + total: number + window: number[] + triggers: number[] lastFireSlot: number | null lastFireTotal: number } -export class SaliencyDetector { - private readonly counters = new Map() +/** + * Debug snapshot for the entire saliency system + */ +export interface SaliencySnapshot { + slot: number + counters: CounterSnapshot[] +} +export class SaliencyDetector { + /** Fixed set of counters - one per event key, never deleted */ + private readonly counters: Map = new Map() + + /** Current slot (advances every slotMs) */ private currentSlot = 0 + /** Timer for advancing slots */ private timer: ReturnType | null = null + /** Milliseconds per slot */ private readonly slotMs = 20 - private lastStatsAt = 0 - private emittedSinceStats: Record = {} - constructor( private readonly deps: { logger: Logg onAttention: (signal: PerceptionSignal) => void rules?: SaliencyRuleBook - windowTicks?: number - threshold?: number }, - ) { } + ) { + // Initialize all counters at construction (fixed set) + for (const key of EVENT_KEYS) { + this.counters.set(key, this.createCounter()) + } + } public start(): void { if (this.timer) @@ -44,12 +79,6 @@ export class SaliencyDetector { this.timer = setInterval(() => { this.currentSlot += 1 this.advanceWindows() - - const now = Date.now() - if (now - this.lastStatsAt >= 2000) { - this.lastStatsAt = now - this.emittedSinceStats = {} - } }, this.slotMs) } @@ -60,6 +89,9 @@ export class SaliencyDetector { this.timer = null } + /** + * Process an incoming perception event + */ public ingest(event: RawPerceptionEvent): void { const rule = this.lookupRule(event) if (!rule) @@ -68,159 +100,135 @@ export class SaliencyDetector { if (rule.predicate && !rule.predicate(event)) return - const tick = this.currentSlot - const windowTicks = rule.windowTicks ?? this.deps.windowTicks ?? DEFAULT_WINDOW_TICKS - const threshold = rule.threshold ?? this.deps.threshold ?? DEFAULT_THRESHOLD - const key = rule.key(event) - - const counter = this.getOrCreateCounter(key, windowTicks) + const counter = this.counters.get(rule.key) + if (!counter) { + // Unknown key - should not happen with fixed key set + return + } + // Increment count in current slot counter.counts[counter.head] = (counter.counts[counter.head] ?? 0) + 1 counter.total += 1 - counter.lastEventSlot = tick + counter.lastEventSlot = this.currentSlot - if (counter.total >= threshold) { - counter.lastFireSlot = tick + // Check threshold + if (counter.total >= rule.threshold) { + counter.lastFireSlot = this.currentSlot counter.lastFireTotal = counter.total counter.triggers[counter.head] = 1 this.resetCounter(counter) - const signal = rule.buildSignal(event) - this.emitSignal(signal) + this.emitSignal(rule.buildSignal(event)) } } - public getDebugSnapshot(options?: { maxKeys?: number }): { - slot: number - keys: Array<{ - key: string - total: number - windowTicks: number - window: number[] - triggers: number[] - lastFireSlot: number | null - lastFireTotal: number - }> - } { - const maxKeys = options?.maxKeys ?? 30 + /** + * Get debug snapshot for visualization + * Returns all counters in fixed order (always same keys, same order) + */ + public getDebugSnapshot(): SaliencySnapshot { + const counters: CounterSnapshot[] = [] - const rows = Array.from(this.counters.entries()).map(([key, counter]) => { - const window = this.exportWindow(counter) - const triggers = this.exportTriggers(counter) - const triggerSum = triggers.reduce((acc, v) => acc + (v ? 1 : 0), 0) - const firedRecently = counter.lastFireSlot !== null && (this.currentSlot - counter.lastFireSlot) <= counter.windowTicks + for (const key of EVENT_KEYS) { + const counter = this.counters.get(key) + if (!counter) + continue - return { + counters.push({ key, total: counter.total, - windowTicks: counter.windowTicks, - window, - triggers, + window: this.exportWindow(counter), + triggers: this.exportTriggers(counter), lastFireSlot: counter.lastFireSlot, lastFireTotal: counter.lastFireTotal, - _triggerSum: triggerSum, - _firedRecently: firedRecently, - } - }) - - // Ensure keys with triggers/recent fires stay visible even if total was reset. - rows.sort((a, b) => { - if (a._triggerSum !== b._triggerSum) - return b._triggerSum - a._triggerSum - - const af = a._firedRecently ? 1 : 0 - const bf = b._firedRecently ? 1 : 0 - if (af !== bf) - return bf - af - - const at = a.lastFireSlot ?? -1 - const bt = b.lastFireSlot ?? -1 - if (at !== bt) - return bt - at - - return b.total - a.total - }) + }) + } return { slot: this.currentSlot, - keys: rows.slice(0, maxKeys).map(({ _triggerSum: _ts, _firedRecently: _fr, ...row }) => row), + counters, } } + /** + * Advance all windows by one slot + * Never deletes counters - they persist with zeroed values + */ private advanceWindows(): void { - for (const [key, counter] of this.counters.entries()) { - counter.head = (counter.head + 1) % counter.windowTicks + for (const counter of this.counters.values()) { + // Move head forward in circular buffer + counter.head = (counter.head + 1) % WINDOW_SIZE + + // Subtract expired slot from total const expired = counter.counts[counter.head] ?? 0 if (expired > 0) { counter.total = Math.max(0, counter.total - expired) - counter.counts[counter.head] = 0 - } - else { - counter.counts[counter.head] = 0 } + // Clear the slot for new data + counter.counts[counter.head] = 0 counter.triggers[counter.head] = 0 - - if (counter.total === 0 && this.currentSlot - counter.lastEventSlot >= counter.windowTicks) { - this.counters.delete(key) - } } } - private getOrCreateCounter(key: string, windowTicks: number): WindowCounter { - const existing = this.counters.get(key) - if (existing && existing.windowTicks === windowTicks) - return existing - - const created: WindowCounter = { - windowTicks, + /** + * Create a new counter with zeroed circular buffer + */ + private createCounter(): WindowCounter { + return { head: 0, - counts: Array.from({ length: windowTicks }, () => 0), - triggers: Array.from({ length: windowTicks }, () => 0), + counts: new Array(WINDOW_SIZE).fill(0), + triggers: new Array(WINDOW_SIZE).fill(0), total: 0, - lastEventSlot: this.currentSlot, + lastEventSlot: 0, lastFireSlot: null, lastFireTotal: 0, } - this.counters.set(key, created) - return created } + /** + * Reset counter counts (after threshold triggered) + */ private resetCounter(counter: WindowCounter): void { counter.total = 0 counter.counts.fill(0) } + /** + * Export window data in chronological order (oldest -> newest) + */ private exportWindow(counter: WindowCounter): number[] { - const w = counter.windowTicks - const out = new Array(w) - // Oldest -> newest. The newest bucket is at `head`. - for (let i = 0; i < w; i++) { - const idx = (counter.head + 1 + i) % w + const out = new Array(WINDOW_SIZE) + for (let i = 0; i < WINDOW_SIZE; i++) { + const idx = (counter.head + 1 + i) % WINDOW_SIZE out[i] = counter.counts[idx] ?? 0 } return out } + /** + * Export trigger markers in chronological order (oldest -> newest) + */ private exportTriggers(counter: WindowCounter): number[] { - const w = counter.windowTicks - const out = new Array(w) - // Oldest -> newest. The newest bucket is at `head`. - for (let i = 0; i < w; i++) { - const idx = (counter.head + 1 + i) % w + const out = new Array(WINDOW_SIZE) + for (let i = 0; i < WINDOW_SIZE; i++) { + const idx = (counter.head + 1 + i) % WINDOW_SIZE out[i] = counter.triggers[idx] ?? 0 } return out } + /** + * Find matching rule for an event + */ private lookupRule(event: RawPerceptionEvent) { const rules = this.deps.rules ?? SALIENCY_RULES return rules[event.modality]?.[event.kind] } + /** + * Emit a perception signal via callback + */ private emitSignal(signal: PerceptionSignal): void { - const key = `emit.${signal.type}.${signal.metadata.action || 'unknown'}` - this.emittedSinceStats[key] = (this.emittedSinceStats[key] ?? 0) + 1 - this.deps.logger.withFields({ type: signal.type, desc: signal.description, diff --git a/services/minecraft/src/cognitive/perception/saliency-rules.ts b/services/minecraft/src/cognitive/perception/saliency-rules.ts index ffcd4c2d1..931610fe2 100644 --- a/services/minecraft/src/cognitive/perception/saliency-rules.ts +++ b/services/minecraft/src/cognitive/perception/saliency-rules.ts @@ -2,148 +2,171 @@ import type { RawPerceptionEvent } from './types/raw-events' import type { PerceptionSignal } from './types/signals' export interface SaliencyRule { - /** - * How many occurrences within the window are required before emitting. - * Defaults to 5. - */ - threshold?: number - /** - * Window size in ticks. Defaults to 100 ticks (~5s). - */ - windowTicks?: number - /** - * Optional predicate to gate the rule. - */ - predicate?: (event: E) => boolean - /** - * Counter key used to bucket occurrences. - */ - key: (event: E) => string - /** - * Builds the PerceptionSignal when the rule fires. - */ - buildSignal: (event: E) => PerceptionSignal + /** + * How many occurrences within the window are required before emitting. + */ + threshold: number + /** + * Optional predicate to gate the rule. + */ + predicate?: (event: E) => boolean + /** + * Counter key - should be one of the predefined EVENT_KEYS + */ + key: string + /** + * Builds the PerceptionSignal when the rule fires. + */ + buildSignal: (event: E) => PerceptionSignal } export type SaliencyRuleBook = Partial + RawPerceptionEvent['modality'], + Record >> -export const DEFAULT_WINDOW_TICKS = 100 +/** + * Fixed event keys for stable heatmap visualization. + * All counters are initialized with these keys at startup. + */ +export const EVENT_KEYS = [ + 'punch:player', + 'teabag:player', + 'move:player', + 'sound:ambient', + 'felt:damage', + 'felt:pickup', +] as const + +export type EventKey = typeof EVENT_KEYS[number] + +/** + * Unified window size for all event types (100 slots = ~2 seconds at 20ms/slot) + */ +export const WINDOW_SIZE = 100 + +/** + * Default threshold for attention triggering + */ export const DEFAULT_THRESHOLD = 5 export const SALIENCY_RULES: SaliencyRuleBook = { - sighted: { - arm_swing: { - key: event => `punch:${(event as Extract).entityId}`, - buildSignal: (event) => { - const e = event as Extract - return { - type: 'entity_attention', - description: `Player ${e.displayName || 'unknown'} is punching nearby`, - sourceId: e.entityId, - confidence: 1.0, - timestamp: Date.now(), - metadata: { - kind: 'player', - action: 'punch', - distance: e.distance, - hasLineOfSight: e.hasLineOfSight, - displayName: e.displayName, - }, - } - }, - }, - sneak_toggle: { - key: event => `teabag:${(event as Extract).entityId}`, - buildSignal: (event) => { - const e = event as Extract - return { - type: 'entity_attention', - description: `Player ${e.displayName || 'unknown'} is teabagging (rapid sneaking)`, - sourceId: e.entityId, - confidence: 1.0, - timestamp: Date.now(), - metadata: { - kind: 'player', - action: 'teabag', - distance: e.distance, - hasLineOfSight: e.hasLineOfSight, - displayName: e.displayName, - }, - } - }, - }, - entity_moved: { - predicate: event => (event as Extract).entityType === 'player', - key: event => `move:${(event as Extract).entityId}`, - buildSignal: (event) => { - const e = event as Extract - return { - type: 'entity_attention', - description: `Player ${e.displayName || 'unknown'} is moving nearby`, - sourceId: e.entityId, - confidence: 0.8, - timestamp: Date.now(), - metadata: { - kind: 'player', - action: 'move', - distance: e.distance, - hasLineOfSight: e.hasLineOfSight, - displayName: e.displayName, - }, - } - }, - }, + sighted: { + arm_swing: { + threshold: 5, + key: 'punch:player', + buildSignal: (event) => { + const e = event as Extract + return { + type: 'entity_attention', + description: `Player ${e.displayName || 'unknown'} is punching nearby`, + sourceId: e.entityId, + confidence: 1.0, + timestamp: Date.now(), + metadata: { + kind: 'player', + action: 'punch', + distance: e.distance, + hasLineOfSight: e.hasLineOfSight, + displayName: e.displayName, + }, + } + }, }, - heard: { - sound: { - key: event => `sound:${(event as Extract).soundId}`, - buildSignal: (event) => { - const e = event as Extract - return { - type: 'environmental_anomaly', - description: `Heard sound: ${e.soundId}`, - sourceId: e.soundId, - confidence: 1.0, - timestamp: Date.now(), - metadata: { - kind: 'sound', - action: 'sound', - soundId: e.soundId, - distance: e.distance, - }, - } - }, - }, + sneak_toggle: { + threshold: 5, + key: 'teabag:player', + buildSignal: (event) => { + const e = event as Extract + return { + type: 'entity_attention', + description: `Player ${e.displayName || 'unknown'} is teabagging (rapid sneaking)`, + sourceId: e.entityId, + confidence: 1.0, + timestamp: Date.now(), + metadata: { + kind: 'player', + action: 'teabag', + distance: e.distance, + hasLineOfSight: e.hasLineOfSight, + displayName: e.displayName, + }, + } + }, }, - felt: { - damage_taken: { - key: () => 'felt:damage', - buildSignal: (_event) => ({ - type: 'saliency_high', - description: 'Taken damage!', - confidence: 1.0, - timestamp: Date.now(), - metadata: { - kind: 'felt', - action: 'damage', - }, - }), - }, - item_collected: { - key: () => 'felt:pickup', - buildSignal: (_event) => ({ - type: 'entity_attention', - description: 'Picked up an item', - confidence: 1.0, - timestamp: Date.now(), - metadata: { - kind: 'felt', - action: 'pickup', - }, - }), - }, + entity_moved: { + threshold: 5, + predicate: event => (event as Extract).entityType === 'player', + key: 'move:player', + buildSignal: (event) => { + const e = event as Extract + return { + type: 'entity_attention', + description: `Player ${e.displayName || 'unknown'} is moving nearby`, + sourceId: e.entityId, + confidence: 0.8, + timestamp: Date.now(), + metadata: { + kind: 'player', + action: 'move', + distance: e.distance, + hasLineOfSight: e.hasLineOfSight, + displayName: e.displayName, + }, + } + }, }, + }, + heard: { + sound: { + threshold: 5, + key: 'sound:ambient', + buildSignal: (event) => { + const e = event as Extract + return { + type: 'environmental_anomaly', + description: `Heard sound: ${e.soundId}`, + sourceId: e.soundId, + confidence: 1.0, + timestamp: Date.now(), + metadata: { + kind: 'sound', + action: 'sound', + soundId: e.soundId, + distance: e.distance, + }, + } + }, + }, + }, + felt: { + damage_taken: { + threshold: 1, // Damage is immediately salient + key: 'felt:damage', + buildSignal: () => ({ + type: 'saliency_high', + description: 'Taken damage!', + confidence: 1.0, + timestamp: Date.now(), + metadata: { + kind: 'felt', + action: 'damage', + }, + }), + }, + item_collected: { + threshold: 3, + key: 'felt:pickup', + buildSignal: () => ({ + type: 'entity_attention', + description: 'Picked up an item', + confidence: 1.0, + timestamp: Date.now(), + metadata: { + kind: 'felt', + action: 'pickup', + }, + }), + }, + }, } diff --git a/services/minecraft/src/debug-server.ts b/services/minecraft/src/debug-server.ts deleted file mode 100644 index 1f841eb46..000000000 --- a/services/minecraft/src/debug-server.ts +++ /dev/null @@ -1,211 +0,0 @@ -import type { IncomingMessage, ServerResponse } from 'node:http' - -import fs from 'node:fs' -import http from 'node:http' -import path from 'node:path' -import process from 'node:process' - -import { fileURLToPath } from 'node:url' - -import { useLogger } from './utils/logger' - -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) - -interface DebugEvent { - type: string - payload: any - timestamp: number -} - -interface LlmTrace { - route: string - messages: any - content: any - usage?: any - model?: string - duration?: number // ms -} - -export class DebugService { - private static instance: DebugService - private clients: Set = new Set() - private server: http.Server | null = null - - // History buffer (Ring buffer) - private history: DebugEvent[] = [] - private readonly MAX_HISTORY = 1000 - - // File Logging - private logStream: fs.WriteStream | null = null - - private constructor() { - this.initLogFile() - } - - public static getInstance(): DebugService { - if (!DebugService.instance) { - DebugService.instance = new DebugService() - } - return DebugService.instance - } - - private initLogFile() { - const logsDir = path.join(process.cwd(), 'logs') - if (!fs.existsSync(logsDir)) { - fs.mkdirSync(logsDir, { recursive: true }) - } - const filename = `session-${new Date().toISOString().replace(/:/g, '-')}.jsonl` - this.logStream = fs.createWriteStream(path.join(logsDir, filename), { flags: 'a' }) - } - - public start(port = 3000): void { - if (this.server) - return - - this.server = http.createServer((req: IncomingMessage, res: ServerResponse) => { - // Enable CORS - res.setHeader('Access-Control-Allow-Origin', '*') - res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS') - - if (req.method === 'OPTIONS') { - res.writeHead(200) - res.end() - return - } - - if (req.url === '/') { - // Serve dashboard - const htmlPath = path.join(__dirname, 'web', 'dashboard.html') - try { - const html = fs.readFileSync(htmlPath, 'utf-8') - res.writeHead(200, { - 'Content-Type': 'text/html', - 'Cache-Control': 'no-cache', - }) - res.end(html) - } - catch { - res.writeHead(500) - res.end('Dashboard not found') - } - } - else if (req.url === '/events') { - // Serve SSE stream - this.handleSSE(req, res) - } - else { - res.writeHead(404) - res.end('Not Found') - } - }) - - this.server.listen(port, () => { - useLogger().log(`Debug server running at http://localhost:${port}`) - }) - } - - // --- Public API for Brain/Agents --- - - public log(level: 'INFO' | 'WARN' | 'ERROR' | 'DEBUG', message: string, fields?: any) { - this.emit('log', { level, message, fields, timestamp: Date.now() }) - } - - public traceLLM(trace: LlmTrace) { - this.emit('llm', { ...trace, timestamp: Date.now() }) - } - - public updateBlackboard(state: any) { - this.emit('blackboard', { state, timestamp: Date.now() }) - } - - public updateQueue(queue: any[], processing?: any) { - this.emit('queue', { queue, processing, timestamp: Date.now() }) - } - - // Generic emit - public emit(type: string, payload: any): void { - const event: DebugEvent = { - type, - payload, - timestamp: Date.now(), - } - - // 1. Add to Histroy - this.addToHistory(event) - - // 2. Persist to Disk - this.persistEvent(event) - - // 3. Broadcast to Clients - this.broadcast(event) - } - - // --- Internal Logic --- - - private addToHistory(event: DebugEvent) { - this.history.push(event) - if (this.history.length > this.MAX_HISTORY) { - this.history.shift() - } - } - - private persistEvent(event: DebugEvent) { - if (this.logStream) { - try { - this.logStream.write(`${JSON.stringify(event)}\n`) - } - catch (err) { - console.error('Failed to write to log file', err) - } - } - } - - private broadcast(event: DebugEvent): void { - // Safe stringify to handle circular refs if any (basic protection) - let data = '' - try { - data = JSON.stringify(event.payload) - } - catch { - data = JSON.stringify({ error: 'Circular Reference or Serialization Error' }) - } - - const message = `event: ${event.type}\ndata: ${data}\n\n` - - for (const client of this.clients) { - client.write(message) - } - } - - private handleSSE(req: IncomingMessage, res: ServerResponse): void { - res.writeHead(200, { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - }) - - // Send history immediately on connection - for (const event of this.history) { - let data = '' - try { - data = JSON.stringify(event.payload) - } - catch { - continue - } - res.write(`event: ${event.type}\ndata: ${data}\n\n`) - } - - this.clients.add(res) - - const keepAlive = setInterval(() => { - res.write(': keep-alive\n\n') - }, 15000) - - req.on('close', () => { - clearInterval(keepAlive) - this.clients.delete(res) - }) - } -} diff --git a/services/minecraft/src/debug/debug-service.ts b/services/minecraft/src/debug/debug-service.ts new file mode 100644 index 000000000..098c828a8 --- /dev/null +++ b/services/minecraft/src/debug/debug-service.ts @@ -0,0 +1,175 @@ +import type { BlackboardEvent, ClientCommand, LLMTraceEvent, LogEvent, QueueEvent, SaliencyEvent, ServerEvent } from './types' + +import { DebugServer } from './server' + +type CommandHandler = (command: ClientCommand) => void + +/** + * DebugService - Public API for application code to emit debug events + * + * This is a singleton that wraps the DebugServer and provides + * a convenient API for emitting events and handling commands. + */ +export class DebugService { + private static instance: DebugService + private server: DebugServer + + private constructor() { + this.server = new DebugServer() + } + + public static getInstance(): DebugService { + if (!DebugService.instance) { + DebugService.instance = new DebugService() + } + return DebugService.instance + } + + /** + * Start the debug server + */ + public start(port = 3000): void { + this.server.start(port) + } + + /** + * Stop the debug server + */ + public stop(): void { + this.server.stop() + } + + // ============================================================ + // Convenience methods for common event types + // ============================================================ + + /** + * Emit a log event + */ + public log(level: LogEvent['level'], message: string, fields?: Record): void { + const event: ServerEvent = { + type: 'log', + payload: { + level, + message, + fields, + timestamp: Date.now(), + }, + } + this.server.broadcast(event) + } + + /** + * Emit an LLM trace event + */ + public traceLLM(trace: Omit): void { + const event: ServerEvent = { + type: 'llm', + payload: { + ...trace, + timestamp: Date.now(), + }, + } + this.server.broadcast(event) + } + + /** + * Emit a blackboard state update + */ + public updateBlackboard(state: BlackboardEvent['state'] | { goal?: string, thought?: string, strategy?: string }): void { + const event: ServerEvent = { + type: 'blackboard', + payload: { + state, + timestamp: Date.now(), + }, + } + this.server.broadcast(event) + } + + /** + * Emit a queue state update + */ + public updateQueue(queue: QueueEvent['queue'], processing?: QueueEvent['processing']): void { + const event: ServerEvent = { + type: 'queue', + payload: { + queue, + processing, + timestamp: Date.now(), + }, + } + this.server.broadcast(event) + } + + /** + * Emit a saliency snapshot + */ + public emitSaliency( + slot: number, + counters: SaliencyEvent['counters'], + ): void { + const event: ServerEvent = { + type: 'saliency', + payload: { + slot, + counters, + timestamp: Date.now(), + }, + } + this.server.broadcast(event) + } + + // ============================================================ + // Generic emit for custom events + // ============================================================ + + /** + * Emit a raw event (for backwards compatibility and custom events) + */ + public emit(type: string, payload: unknown): void { + // Map to strongly-typed events where possible + switch (type) { + case 'log': + this.server.broadcast({ type: 'log', payload: payload as LogEvent }) + break + case 'llm': + this.server.broadcast({ type: 'llm', payload: payload as LLMTraceEvent }) + break + case 'blackboard': + this.server.broadcast({ type: 'blackboard', payload: payload as BlackboardEvent }) + break + case 'queue': + this.server.broadcast({ type: 'queue', payload: payload as QueueEvent }) + break + case 'saliency': { + // Transform the saliency data to match expected format + const data = payload as { slot?: number, counters?: SaliencyEvent['counters'] } + this.server.broadcast({ + type: 'saliency', + payload: { + slot: data.slot || 0, + counters: data.counters || [], + timestamp: Date.now(), + }, + }) + break + } + default: + // For unknown types, emit as log + this.log('DEBUG', `Unknown event type: ${type}`, { payload }) + } + } + + // ============================================================ + // Command handling + // ============================================================ + + /** + * Register a handler for client commands + * Returns an unsubscribe function + */ + public onCommand(type: string, handler: CommandHandler): () => void { + return this.server.onCommand(type, handler) + } +} diff --git a/services/minecraft/src/debug/index.ts b/services/minecraft/src/debug/index.ts new file mode 100644 index 000000000..62f4c8267 --- /dev/null +++ b/services/minecraft/src/debug/index.ts @@ -0,0 +1,13 @@ +// Re-export public API +export { DebugService } from './debug-service' +export { DebugServer } from './server' +export type { + BlackboardEvent, + ClientCommand, + DebugMessage, + LLMTraceEvent, + LogEvent, + QueueEvent, + SaliencyEvent, + ServerEvent, +} from './types' diff --git a/services/minecraft/src/debug/server.ts b/services/minecraft/src/debug/server.ts new file mode 100644 index 000000000..d22b4742a --- /dev/null +++ b/services/minecraft/src/debug/server.ts @@ -0,0 +1,334 @@ +import type { IncomingMessage } from 'node:http' + +import type { WebSocket } from 'ws' + +import type { ClientCommand, DebugMessage, ServerEvent } from './types' + +import fs from 'node:fs' +import http from 'node:http' +import path from 'node:path' +import process from 'node:process' + +import { fileURLToPath } from 'node:url' + +import { WebSocketServer } from 'ws' + +import { useLogger } from '../utils/logger' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +interface ClientInfo { + ws: WebSocket + id: string + connectedAt: number + lastPing: number +} + +type CommandHandler = (command: ClientCommand, client: ClientInfo) => void + +export class DebugServer { + private httpServer: http.Server | null = null + private wss: WebSocketServer | null = null + private clients: Map = new Map() + private commandHandlers: Map = new Map() + + // History buffer (Ring buffer) + private history: ServerEvent[] = [] + private readonly MAX_HISTORY = 1000 + + // File Logging + private logStream: fs.WriteStream | null = null + + // Heartbeat + private heartbeatInterval: ReturnType | null = null + private readonly HEARTBEAT_INTERVAL = 30000 + + private messageIdCounter = 0 + + constructor() { + this.initLogFile() + } + + private initLogFile(): void { + const logsDir = path.join(process.cwd(), 'logs') + if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }) + } + const filename = `session-${new Date().toISOString().replace(/:/g, '-')}.jsonl` + this.logStream = fs.createWriteStream(path.join(logsDir, filename), { flags: 'a' }) + } + + public start(port = 3000): void { + if (this.httpServer) { + return + } + + // Create HTTP server for static files + this.httpServer = http.createServer((req, res) => { + this.handleHttpRequest(req, res) + }) + + // Create WebSocket server + this.wss = new WebSocketServer({ server: this.httpServer }) + + this.wss.on('connection', (ws, req) => { + this.handleConnection(ws, req) + }) + + // Start heartbeat + this.heartbeatInterval = setInterval(() => { + this.sendHeartbeat() + }, this.HEARTBEAT_INTERVAL) + + this.httpServer.listen(port, () => { + useLogger().log(`Debug server running at http://localhost:${port}`) + }) + } + + public stop(): void { + if (this.heartbeatInterval) { + clearInterval(this.heartbeatInterval) + this.heartbeatInterval = null + } + + for (const client of this.clients.values()) { + client.ws.close() + } + this.clients.clear() + + this.wss?.close() + this.wss = null + + this.httpServer?.close() + this.httpServer = null + + this.logStream?.end() + this.logStream = null + } + + // ============================================================ + // Public API for emitting events + // ============================================================ + + public broadcast(event: ServerEvent): void { + // Add to history + this.addToHistory(event) + + // Persist to disk + this.persistEvent(event) + + // Send to all connected clients + const message = this.createMessage(event) + const data = JSON.stringify(message) + + for (const client of this.clients.values()) { + if (client.ws.readyState === 1) { // WebSocket.OPEN + client.ws.send(data) + } + } + } + + // ============================================================ + // Command handling + // ============================================================ + + public onCommand(type: string, handler: CommandHandler): () => void { + const handlers = this.commandHandlers.get(type) || [] + handlers.push(handler) + this.commandHandlers.set(type, handlers) + + // Return unsubscribe function + return () => { + const h = this.commandHandlers.get(type) + if (h) { + const idx = h.indexOf(handler) + if (idx !== -1) { + h.splice(idx, 1) + } + } + } + } + + // ============================================================ + // Private methods + // ============================================================ + + private handleHttpRequest(req: IncomingMessage, res: http.ServerResponse): void { + // Enable CORS + res.setHeader('Access-Control-Allow-Origin', '*') + res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS') + + if (req.method === 'OPTIONS') { + res.writeHead(200) + res.end() + return + } + + // Serve static files from web directory + let filePath = req.url === '/' ? '/index.html' : req.url || '/index.html' + + // Security: prevent directory traversal + filePath = path.normalize(filePath).replace(/^(\.\.[/\\])+/, '') + + const fullPath = path.join(__dirname, 'web', filePath) + const extname = path.extname(fullPath).toLowerCase() + + const mimeTypes: Record = { + '.html': 'text/html', + '.js': 'application/javascript', + '.css': 'text/css', + '.json': 'application/json', + '.png': 'image/png', + '.svg': 'image/svg+xml', + } + + const contentType = mimeTypes[extname] || 'application/octet-stream' + + fs.readFile(fullPath, (err, content) => { + if (err) { + if (err.code === 'ENOENT') { + res.writeHead(404) + res.end('Not Found') + } + else { + res.writeHead(500) + res.end('Server Error') + } + return + } + + res.writeHead(200, { + 'Content-Type': contentType, + 'Cache-Control': 'no-cache', + }) + res.end(content) + }) + } + + private handleConnection(ws: WebSocket, _req: IncomingMessage): void { + const clientId = this.generateClientId() + const client: ClientInfo = { + ws, + id: clientId, + connectedAt: Date.now(), + lastPing: Date.now(), + } + + this.clients.set(clientId, client) + + // Send history on connect + this.sendHistory(client) + + ws.on('message', (data) => { + this.handleMessage(client, data.toString()) + }) + + ws.on('close', () => { + this.clients.delete(clientId) + }) + + ws.on('error', (err) => { + console.error(`WebSocket error for client ${clientId}:`, err) + this.clients.delete(clientId) + }) + } + + private handleMessage(client: ClientInfo, data: string): void { + try { + const message = JSON.parse(data) as DebugMessage + const command = message.data + + // Handle ping internally + if (command.type === 'ping') { + client.lastPing = Date.now() + const pong: ServerEvent = { type: 'pong', payload: { timestamp: Date.now() } } + client.ws.send(JSON.stringify(this.createMessage(pong))) + return + } + + // Handle history request + if (command.type === 'request_history') { + this.sendHistory(client) + return + } + + // Dispatch to registered handlers + const handlers = this.commandHandlers.get(command.type) + if (handlers) { + for (const handler of handlers) { + try { + handler(command, client) + } + catch (err) { + console.error(`Command handler error for ${command.type}:`, err) + } + } + } + } + catch (err) { + console.error('Failed to parse WebSocket message:', err) + } + } + + private sendHistory(client: ClientInfo): void { + if (this.history.length === 0) { + return + } + + const historyEvent: ServerEvent = { + type: 'history', + payload: this.history, + } + + client.ws.send(JSON.stringify(this.createMessage(historyEvent))) + } + + private sendHeartbeat(): void { + const now = Date.now() + for (const [clientId, client] of this.clients) { + // Check if client is still alive (responded to ping within 2x heartbeat interval) + if (now - client.lastPing > this.HEARTBEAT_INTERVAL * 2) { + client.ws.close() + this.clients.delete(clientId) + continue + } + + // Send ping request + if (client.ws.readyState === 1) { + const ping: ServerEvent = { type: 'pong', payload: { timestamp: now } } + client.ws.send(JSON.stringify(this.createMessage(ping))) + } + } + } + + private addToHistory(event: ServerEvent): void { + this.history.push(event) + if (this.history.length > this.MAX_HISTORY) { + this.history.shift() + } + } + + private persistEvent(event: ServerEvent): void { + if (this.logStream) { + try { + this.logStream.write(`${JSON.stringify(event)}\n`) + } + catch (err) { + console.error('Failed to write to log file', err) + } + } + } + + private createMessage(event: ServerEvent): DebugMessage { + return { + id: `${++this.messageIdCounter}`, + data: event, + timestamp: Date.now(), + } + } + + private generateClientId(): string { + return `client-${Date.now()}-${Math.random().toString(36).substring(2, 9)}` + } +} diff --git a/services/minecraft/src/debug/types.ts b/services/minecraft/src/debug/types.ts new file mode 100644 index 000000000..df87768c8 --- /dev/null +++ b/services/minecraft/src/debug/types.ts @@ -0,0 +1,119 @@ +// Debug server message types for bidirectional WebSocket communication + +// ============================================================ +// Server -> Client events +// ============================================================ + +export interface LogEvent { + level: 'INFO' | 'WARN' | 'ERROR' | 'DEBUG' + message: string + fields?: Record + timestamp: number +} + +export interface LLMTraceEvent { + route: string + messages: unknown[] + content: string + usage?: { + prompt_tokens?: number + completion_tokens?: number + total_tokens?: number + } + model?: string + duration?: number // ms + timestamp: number +} + +export interface BlackboardEvent { + state: Record + timestamp: number +} + +export interface QueueEvent { + queue: Array<{ + type: string + payload: unknown + source?: { type: string, id: string } + }> + processing?: { + type: string + payload: unknown + source?: { type: string, id: string } + } + timestamp: number +} + +export interface SaliencyEvent { + slot: number + counters: Array<{ + key: string + total: number + window: number[] + triggers: number[] + lastFireSlot: number | null + lastFireTotal: number + }> + timestamp: number +} + +// Union type for all server events +export type ServerEvent + = | { type: 'log', payload: LogEvent } + | { type: 'llm', payload: LLMTraceEvent } + | { type: 'blackboard', payload: BlackboardEvent } + | { type: 'queue', payload: QueueEvent } + | { type: 'saliency', payload: SaliencyEvent } + | { type: 'history', payload: ServerEvent[] } + | { type: 'pong', payload: { timestamp: number } } + +// ============================================================ +// Client -> Server commands +// ============================================================ + +export interface ClearLogsCommand { + type: 'clear_logs' +} + +export interface SetFilterCommand { + type: 'set_filter' + payload: { + panel: string + filter: string + } +} + +export interface InjectEventCommand { + type: 'inject_event' + payload: { + eventType: string + data: unknown + } +} + +export interface PingCommand { + type: 'ping' + payload: { timestamp: number } +} + +export interface RequestHistoryCommand { + type: 'request_history' +} + +// Union type for all client commands +export type ClientCommand + = | ClearLogsCommand + | SetFilterCommand + | InjectEventCommand + | PingCommand + | RequestHistoryCommand + +// ============================================================ +// Wire format +// ============================================================ + +export interface DebugMessage { + id: string + data: T + timestamp: number +} diff --git a/services/minecraft/src/debug/web/app.js b/services/minecraft/src/debug/web/app.js new file mode 100644 index 000000000..4cca69405 --- /dev/null +++ b/services/minecraft/src/debug/web/app.js @@ -0,0 +1,654 @@ +/** + * Debug Dashboard - Multi-View Application + * All panels visible simultaneously, focus on robustness and utility + */ + +// ============================================================================= +// Configuration & Constants +// ============================================================================= + +const CONFIG = { + MAX_LOGS: 500, + MAX_LLM_TRACES: 50, + RECONNECT_MAX_ATTEMPTS: 10, + RECONNECT_DELAY: 1000, + PING_INTERVAL: 25000, + UPDATE_THROTTLE: 100, // ms +} + +// ============================================================================= +// Utility Functions +// ============================================================================= + +function escapeHtml(text) { + const div = document.createElement('div') + div.textContent = text + return div.innerHTML +} + +function throttle(func, wait) { + let timeout + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout) + func(...args) + } + clearTimeout(timeout) + timeout = setTimeout(later, wait) + } +} + +// ============================================================================= +// WebSocket Client +// ============================================================================= + +class DebugClient { + constructor() { + this.ws = null + this.reconnectAttempts = 0 + this.pingInterval = null + this.messageIdCounter = 0 + this.eventHandlers = new Map() + } + + on(eventType, handler) { + if (!this.eventHandlers.has(eventType)) { + this.eventHandlers.set(eventType, []) + } + this.eventHandlers.get(eventType).push(handler) + } + + emit(eventType, data) { + const handlers = this.eventHandlers.get(eventType) + if (handlers) { + handlers.forEach((handler) => { + try { + handler(data) + } + catch (err) { + console.error(`Error in ${eventType} handler:`, err) + } + }) + } + } + + connect() { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' + const wsUrl = `${protocol}//${window.location.host}` + + this.ws = new WebSocket(wsUrl) + + this.ws.onopen = () => { + this.reconnectAttempts = 0 + this.updateConnectionStatus(true) + this.startPing() + this.emit('connected') + } + + this.ws.onclose = () => { + this.updateConnectionStatus(false) + this.stopPing() + this.scheduleReconnect() + } + + this.ws.onerror = (err) => { + console.error('WebSocket error:', err) + } + + this.ws.onmessage = (event) => { + this.handleMessage(event.data) + } + } + + disconnect() { + if (this.ws) { + this.ws.close() + this.ws = null + } + this.stopPing() + } + + reconnect() { + this.disconnect() + this.reconnectAttempts = 0 + this.connect() + } + + send(command) { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + const message = { + id: `${++this.messageIdCounter}`, + data: command, + timestamp: Date.now(), + } + this.ws.send(JSON.stringify(message)) + } + } + + handleMessage(data) { + try { + const message = JSON.parse(data) + const event = message.data + + if (event.type === 'history') { + for (const historyEvent of event.payload) { + this.routeEvent(historyEvent) + } + return + } + + if (event.type === 'pong') { + return + } + + this.routeEvent(event) + } + catch (err) { + console.error('Failed to parse message:', err) + } + } + + routeEvent(event) { + this.emit(event.type, event.payload) + } + + updateConnectionStatus(connected) { + const dot = document.getElementById('connection-status') + const text = document.getElementById('status-text') + + if (connected) { + dot.classList.add('connected') + text.textContent = 'Connected' + } + else { + dot.classList.remove('connected') + text.textContent = 'Disconnected' + } + } + + scheduleReconnect() { + if (this.reconnectAttempts >= CONFIG.RECONNECT_MAX_ATTEMPTS) { + console.log('Max reconnect attempts reached') + return + } + + this.reconnectAttempts++ + const delay = CONFIG.RECONNECT_DELAY * Math.min(this.reconnectAttempts, 5) + + const text = document.getElementById('status-text') + text.textContent = `Reconnecting (${this.reconnectAttempts})...` + + setTimeout(() => this.connect(), delay) + } + + startPing() { + this.pingInterval = setInterval(() => { + this.send({ type: 'ping', payload: { timestamp: Date.now() } }) + }, CONFIG.PING_INTERVAL) + } + + stopPing() { + if (this.pingInterval) { + clearInterval(this.pingInterval) + this.pingInterval = null + } + } +} + +// ============================================================================= +// Queue Panel +// ============================================================================= + +class QueuePanel { + constructor(client) { + this.client = client + this.data = { queue: [], processing: null } + this.elements = { + queueList: document.getElementById('queue-list'), + queueCount: document.getElementById('queue-count'), + processingContent: document.getElementById('processing-content'), + statQueue: document.getElementById('stat-queue'), + } + } + + init() { + this.client.on('queue', data => this.update(data)) + this.client.on('connected', () => this.reset()) + this.render() + } + + update(data) { + this.data = data + this.render() + } + + reset() { + this.data = { queue: [], processing: null } + this.render() + } + + render() { + const { queue, processing } = this.data + + // Queue count + const queueSize = queue?.length || 0 + this.elements.queueCount.textContent = queueSize + this.elements.statQueue.textContent = queueSize + + // Queue list + if (queue && queue.length > 0) { + this.elements.queueList.innerHTML = queue.map((item, idx) => ` +
+ #${idx + 1} + ${escapeHtml(item.type)} + ${escapeHtml(item.source?.id || 'unknown')} +
+ `).join('') + } + else { + this.elements.queueList.innerHTML = '
Queue empty
' + } + + // Processing + if (processing) { + this.elements.processingContent.innerHTML = ` +
+ ${escapeHtml(processing.type)} +
+ Source: ${escapeHtml(processing.source?.type)}/${escapeHtml(processing.source?.id)} +
+
+ ` + } + else { + this.elements.processingContent.innerHTML = 'Idle' + } + } +} + +// ============================================================================= +// Blackboard Panel +// ============================================================================= + +class BlackboardPanel { + constructor(client) { + this.client = client + this.state = {} + this.elements = { + json: document.getElementById('blackboard-json'), + copyBtn: document.getElementById('blackboard-copy-btn'), + } + } + + init() { + this.client.on('blackboard', data => this.update(data)) + this.client.on('connected', () => this.reset()) + this.elements.copyBtn.addEventListener('click', () => this.copy()) + this.render() + } + + update(data) { + this.state = data.state || {} + this.render() + } + + reset() { + this.state = {} + this.render() + } + + render() { + this.elements.json.textContent = JSON.stringify(this.state, null, 2) + } + + copy() { + navigator.clipboard.writeText(JSON.stringify(this.state, null, 2)) + .then(() => { + this.elements.copyBtn.textContent = '✓' + setTimeout(() => { this.elements.copyBtn.textContent = '📋' }, 1000) + }) + .catch(err => console.error('Copy failed:', err)) + } +} + +// ============================================================================= +// Logs Panel +// ============================================================================= + +class LogsPanel { + constructor(client) { + this.client = client + this.logs = [] + this.autoScroll = true + this.paused = false + this.filter = { level: 'all', search: '' } + this.elements = { + container: document.getElementById('logs-container'), + search: document.getElementById('log-search'), + levelFilter: document.getElementById('log-level-filter'), + autoScroll: document.getElementById('auto-scroll'), + statEvents: document.getElementById('stat-events'), + } + } + + init() { + this.client.on('log', data => this.addLog(data)) + this.client.on('connected', () => this.reset()) + + this.elements.search.addEventListener('input', (e) => { + this.filter.search = e.target.value.toLowerCase() + this.renderThrottled() + }) + + this.elements.levelFilter.addEventListener('change', (e) => { + this.filter.level = e.target.value + this.renderThrottled() + }) + + this.elements.autoScroll.addEventListener('change', (e) => { + this.autoScroll = e.target.checked + }) + + this.renderThrottled = throttle(() => this.render(), CONFIG.UPDATE_THROTTLE) + this.render() + } + + addLog(entry) { + if (this.paused) + return + + this.logs.push(entry) + if (this.logs.length > CONFIG.MAX_LOGS) { + this.logs.shift() + } + + this.elements.statEvents.textContent = this.logs.length + this.renderThrottled() + } + + reset() { + this.logs = [] + this.elements.statEvents.textContent = '0' + this.render() + } + + clear() { + this.logs = [] + this.elements.statEvents.textContent = '0' + this.render() + } + + setPaused(paused) { + this.paused = paused + } + + render() { + const filtered = this.logs.filter((log) => { + if (this.filter.level !== 'all' && log.level !== this.filter.level) { + return false + } + if (this.filter.search && !log.message.toLowerCase().includes(this.filter.search)) { + return false + } + return true + }) + + this.elements.container.innerHTML = filtered.map((log) => { + const time = new Date(log.timestamp).toLocaleTimeString() + const fieldsStr = log.fields ? JSON.stringify(log.fields) : '' + + return ` +
+ ${time} + [${log.level}] + ${escapeHtml(log.message)} + ${fieldsStr ? `
${escapeHtml(fieldsStr)}
` : ''} +
+ ` + }).join('') + + if (this.autoScroll) { + this.elements.container.scrollTop = this.elements.container.scrollHeight + } + } +} + +// ============================================================================= +// LLM Panel +// ============================================================================= + +class LLMPanel { + constructor(client) { + this.client = client + this.traces = [] + this.elements = { + list: document.getElementById('llm-list'), + count: document.getElementById('llm-count'), + statLlm: document.getElementById('stat-llm'), + } + } + + init() { + this.client.on('llm', data => this.addTrace(data)) + this.client.on('connected', () => this.reset()) + } + + addTrace(trace) { + this.traces.unshift(trace) + if (this.traces.length > CONFIG.MAX_LLM_TRACES) { + this.traces.pop() + } + + this.elements.count.textContent = this.traces.length + this.elements.statLlm.textContent = this.traces.length + this.renderTrace(trace) + } + + reset() { + this.traces = [] + this.elements.list.innerHTML = '' + this.elements.count.textContent = '0' + this.elements.statLlm.textContent = '0' + } + + renderTrace(trace) { + const time = new Date(trace.timestamp).toLocaleTimeString() + const tokens = trace.usage?.total_tokens || '?' + const duration = Number.isFinite(trace.duration) ? `${trace.duration}ms` : 'n/a' + + const card = document.createElement('div') + card.className = 'llm-card' + + // Messages HTML + let messagesHtml = '' + if (trace.messages && Array.isArray(trace.messages)) { + messagesHtml = trace.messages.map(msg => ` +
+
${msg.role || 'unknown'}
+
${escapeHtml(msg.content || '')}
+
+ `).join('') + } + + card.innerHTML = ` +
+
+ ${escapeHtml(trace.route || 'unknown')} + ${time} +
+
+ ${tokens} tokens + ${duration} +
+
+
+
Result
+
${escapeHtml(trace.content || '')}
+ +
Messages
+
+ ${messagesHtml || '
No messages
'} +
+ +
Usage
+
${JSON.stringify(trace.usage || {}, null, 2)}
+
+ ` + + const header = card.querySelector('.llm-header') + const body = card.querySelector('.llm-body') + header.addEventListener('click', () => { + body.classList.toggle('open') + }) + + this.elements.list.insertBefore(card, this.elements.list.firstChild) + } +} + +// ============================================================================= +// Saliency Panel +// ============================================================================= + +class SaliencyPanel { + constructor(client) { + this.client = client + this.data = null + this.elements = { + canvas: document.getElementById('saliency-canvas'), + labels: document.getElementById('saliency-labels'), + slot: document.getElementById('saliency-slot'), + } + this.ctx = this.elements.canvas.getContext('2d') + } + + init() { + this.client.on('saliency', data => this.update(data)) + this.client.on('connected', () => this.reset()) + } + + update(data) { + this.data = data + this.render() + } + + reset() { + this.data = null + this.render() + } + + render() { + if (!this.data || !this.data.counters) { + this.elements.labels.innerHTML = '
No data
' + return + } + + this.elements.slot.textContent = this.data.slot || 0 + + // Use all counters directly from server (fixed order) + const counters = this.data.counters + + // Render labels + this.elements.labels.innerHTML = counters.map(c => ` +
+ ${escapeHtml(c.key || '')} + (${c.total || 0}) +
+ `).join('') + + // Render canvas + const cols = 100 + const rows = counters.length + const cellW = 6 + const cellH = 16 + + this.elements.canvas.width = cols * cellW + this.elements.canvas.height = Math.max(1, rows) * cellH + + this.ctx.fillStyle = '#0d1117' + this.ctx.fillRect(0, 0, this.elements.canvas.width, this.elements.canvas.height) + + let maxCell = 0 + for (const counter of counters) { + for (let i = 0; i < Math.min(cols, counter.window?.length || 0); i++) { + maxCell = Math.max(maxCell, counter.window[i] || 0) + } + } + + for (let y = 0; y < rows; y++) { + const counter = counters[y] + const w = counter.window || [] + const t = counter.triggers || [] + + for (let x = 0; x < cols; x++) { + const v = x < w.length ? (w[x] || 0) : 0 + const fired = x < t.length ? (t[x] || 0) : 0 + + this.ctx.fillStyle = fired + ? 'rgba(248, 81, 73, 0.9)' + : this.colorFor(v, maxCell) + this.ctx.fillRect(x * cellW, y * cellH, cellW - 1, cellH - 1) + } + } + } + + colorFor(value, maxValue) { + if (!maxValue) + return 'rgba(0,0,0,0)' + const t = Math.min(1, value / maxValue) + const r = Math.round(88 + 80 * t) + const g = Math.round(166 + 80 * t) + const b = Math.round(255 * t) + return `rgba(${r},${g},${b},${0.2 + 0.8 * t})` + } +} + +// ============================================================================= +// Application +// ============================================================================= + +class DebugApp { + constructor() { + this.client = new DebugClient() + this.panels = { + queue: new QueuePanel(this.client), + blackboard: new BlackboardPanel(this.client), + logs: new LogsPanel(this.client), + llm: new LLMPanel(this.client), + saliency: new SaliencyPanel(this.client), + } + this.paused = false + } + + init() { + // Initialize all panels + Object.values(this.panels).forEach(panel => panel.init()) + + // Setup controls + document.getElementById('clear-logs-btn').addEventListener('click', () => { + this.panels.logs.clear() + }) + + document.getElementById('pause-btn').addEventListener('click', (e) => { + this.paused = !this.paused + this.panels.logs.setPaused(this.paused) + e.target.textContent = this.paused ? 'Resume' : 'Pause' + }) + + document.getElementById('reconnect-btn').addEventListener('click', () => { + this.client.reconnect() + }) + + // Connect + this.client.connect() + } +} + +// ============================================================================= +// Bootstrap +// ============================================================================= + +document.addEventListener('DOMContentLoaded', () => { + const app = new DebugApp() + app.init() + window.debugApp = app // For debugging +}) diff --git a/services/minecraft/src/debug/web/index.html b/services/minecraft/src/debug/web/index.html new file mode 100644 index 000000000..05dcb08fe --- /dev/null +++ b/services/minecraft/src/debug/web/index.html @@ -0,0 +1,129 @@ + + + + + + + Debug Dashboard + + + + + + + + +
+ +
+ +
+
+

Queue & Processing

+ 0 +
+
+
+
+
+
Processing:
+
Idle
+
+
+
+ + +
+
+

Blackboard

+ +
+
+
{}
+
+
+
+ + +
+
+

Logs

+
+ + + +
+
+
+
+
+
+ + +
+
+

LLM Traces

+ 0 +
+
+
+
+
+ + +
+
+

Saliency Heatmap

+
+ Slot: 0 +
+
+
+
+ +
+
+
+
+
+ + + + + \ No newline at end of file diff --git a/services/minecraft/src/debug/web/styles.css b/services/minecraft/src/debug/web/styles.css new file mode 100644 index 000000000..be385f556 --- /dev/null +++ b/services/minecraft/src/debug/web/styles.css @@ -0,0 +1,554 @@ +/* ========================================================================== + Debug Dashboard - Utility-Focused Styles + ========================================================================== */ + +/* CSS Variables */ +:root { + --bg-primary: #0d1117; + --bg-secondary: #161b22; + --bg-tertiary: #21262d; + --border-color: #30363d; + --text-primary: #c9d1d9; + --text-secondary: #8b949e; + --text-muted: #6e7681; + --accent-info: #58a6ff; + --accent-success: #3fb950; + --accent-warning: #d29922; + --accent-error: #f85149; + --font-mono: 'Consolas', 'Monaco', 'Courier New', monospace; + --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; +} + +/* Base Styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, +body { + height: 100%; + overflow: hidden; +} + +body { + font-family: var(--font-sans); + font-size: 13px; + line-height: 1.5; + color: var(--text-primary); + background-color: var(--bg-primary); +} + +/* Header */ +#header { + display: flex; + align-items: center; + gap: 1rem; + padding: 0.5rem 1rem; + background-color: var(--bg-secondary); + border-bottom: 1px solid var(--border-color); + height: 48px; + flex-shrink: 0; +} + +.header-left { + display: flex; + align-items: center; + gap: 1rem; +} + +.header-left h1 { + font-size: 14px; + font-weight: 600; + color: var(--text-primary); +} + +.status-indicator { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 12px; +} + +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background-color: var(--accent-error); +} + +.status-dot.connected { + background-color: var(--accent-success); +} + +.header-stats { + display: flex; + gap: 1.5rem; + margin-left: auto; +} + +.stat { + display: flex; + flex-direction: column; + align-items: center; +} + +.stat-label { + font-size: 10px; + color: var(--text-muted); + text-transform: uppercase; +} + +.stat-value { + font-size: 16px; + font-weight: 600; + font-family: var(--font-mono); + color: var(--accent-info); +} + +.header-controls { + display: flex; + gap: 0.5rem; +} + +/* Buttons */ +button, +.icon-btn { + padding: 0.25rem 0.75rem; + font-size: 12px; + font-family: inherit; + background-color: var(--bg-tertiary); + color: var(--text-primary); + border: 1px solid var(--border-color); + border-radius: 3px; + cursor: pointer; + transition: background-color 0.1s; +} + +button:hover, +.icon-btn:hover { + background-color: var(--border-color); +} + +.icon-btn { + padding: 0.25rem 0.5rem; +} + +/* Main Grid Layout */ +#main-grid { + display: grid; + grid-template-columns: 350px 1fr; + grid-template-rows: minmax(300px, 1fr) minmax(200px, 0.6fr) minmax(200px, 0.4fr); + gap: 8px; + padding: 8px; + height: calc(100vh - 48px); + overflow: hidden; +} + +#left-column { + display: flex; + flex-direction: column; + gap: 8px; + grid-row: 1 / 2; +} + +#queue-section { + flex: 0 0 auto; + max-height: 40%; + min-height: 180px; +} + +#blackboard-section { + flex: 1; + min-height: 0; +} + +#logs-section { + grid-row: 1 / 2; + grid-column: 2 / 3; +} + +#llm-section { + grid-row: 2 / 3; + grid-column: 1 / 3; +} + +#saliency-section { + grid-row: 3 / 4; + grid-column: 1 / 3; +} + +/* Panel Styles */ +.panel { + display: flex; + flex-direction: column; + background-color: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 4px; + overflow: hidden; +} + +.panel-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.5rem 0.75rem; + background-color: var(--bg-tertiary); + border-bottom: 1px solid var(--border-color); + flex-shrink: 0; +} + +.panel-header h2 { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); +} + +.panel-badge { + padding: 2px 6px; + font-size: 11px; + font-family: var(--font-mono); + background-color: var(--bg-primary); + color: var(--accent-info); + border-radius: 3px; +} + +.panel-controls { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.panel-controls input[type="search"], +.panel-controls input[type="number"], +.panel-controls select { + padding: 0.25rem 0.5rem; + font-size: 12px; + font-family: inherit; + background-color: var(--bg-primary); + color: var(--text-primary); + border: 1px solid var(--border-color); + border-radius: 3px; +} + +.panel-controls input[type="search"] { + width: 200px; +} + +.checkbox-label { + display: flex; + align-items: center; + gap: 0.25rem; + font-size: 12px; + color: var(--text-secondary); + cursor: pointer; +} + +.panel-content { + flex: 1; + overflow: auto; + padding: 0.5rem; + min-height: 0; +} + +/* Queue & Processing */ +.queue-list { + display: flex; + flex-direction: column; + gap: 4px; + margin-bottom: 0.5rem; +} + +.queue-item { + padding: 0.25rem 0.5rem; + background-color: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: 3px; + font-size: 12px; + font-family: var(--font-mono); +} + +.queue-item .queue-index { + color: var(--text-muted); + margin-right: 0.5rem; +} + +.queue-item .queue-type { + color: var(--accent-info); +} + +.divider { + height: 1px; + background-color: var(--border-color); + margin: 0.5rem 0; +} + +.processing-info { + padding: 0.5rem; + background-color: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: 3px; +} + +.processing-label { + font-size: 11px; + color: var(--text-muted); + text-transform: uppercase; + margin-bottom: 0.25rem; +} + +#processing-content { + font-size: 12px; + font-family: var(--font-mono); + white-space: pre-wrap; +} + +.processing-active { + color: var(--accent-success); +} + +/* Blackboard */ +.json-display { + font-family: var(--font-mono); + font-size: 12px; + line-height: 1.4; + color: var(--text-primary); + white-space: pre-wrap; + word-wrap: break-word; +} + +/* Logs */ +.logs-container { + font-family: var(--font-mono); + font-size: 12px; + line-height: 1.4; +} + +.log-entry { + padding: 2px 0; + border-bottom: 1px solid var(--bg-tertiary); +} + +.log-time { + color: var(--text-muted); + margin-right: 0.5rem; +} + +.log-level { + margin-right: 0.5rem; + font-weight: 600; +} + +.level-ERROR { + color: var(--accent-error); +} + +.level-WARN { + color: var(--accent-warning); +} + +.level-INFO { + color: var(--accent-info); +} + +.level-DEBUG { + color: var(--text-muted); +} + +.level-SYSTEM { + color: var(--accent-success); +} + +.log-message { + color: var(--text-primary); +} + +.log-fields { + color: var(--text-secondary); + font-size: 11px; + margin-top: 2px; +} + +/* LLM Traces */ +.llm-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.llm-card { + background-color: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: 3px; + overflow: hidden; +} + +.llm-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem; + background-color: var(--bg-tertiary); + cursor: pointer; + font-size: 12px; +} + +.llm-header:hover { + background-color: var(--border-color); +} + +.llm-title { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.llm-route { + color: var(--accent-info); + font-family: var(--font-mono); +} + +.llm-meta { + display: flex; + gap: 0.75rem; + font-family: var(--font-mono); + font-size: 11px; + color: var(--text-secondary); +} + +.llm-body { + display: none; + padding: 0.75rem; + border-top: 1px solid var(--border-color); +} + +.llm-body.open { + display: block; +} + +.llm-section-title { + font-size: 11px; + font-weight: 600; + color: var(--text-muted); + text-transform: uppercase; + margin-top: 0.75rem; + margin-bottom: 0.25rem; +} + +.llm-section-title:first-child { + margin-top: 0; +} + +.llm-content { + padding: 0.5rem; + background-color: var(--bg-secondary); + border-radius: 3px; + font-family: var(--font-mono); + font-size: 12px; + white-space: pre-wrap; + word-wrap: break-word; +} + +.llm-messages { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.llm-message { + padding: 0.5rem; + background-color: var(--bg-secondary); + border-left: 3px solid transparent; + border-radius: 3px; +} + +.llm-message.role-system { + border-left-color: #a371f7; +} + +.llm-message.role-user { + border-left-color: var(--accent-success); +} + +.llm-message.role-assistant { + border-left-color: var(--accent-info); +} + +.llm-message-role { + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + color: var(--text-muted); + margin-bottom: 0.25rem; +} + +.llm-message-content { + font-family: var(--font-mono); + font-size: 12px; + white-space: pre-wrap; + word-wrap: break-word; +} + +/* Saliency */ +.saliency-container { + display: flex; + gap: 1rem; +} + +#saliency-canvas { + border: 1px solid var(--border-color); + background-color: var(--bg-primary); +} + +.saliency-labels { + flex: 0 0 300px; + font-family: var(--font-mono); + font-size: 11px; + line-height: 16px; + overflow-y: auto; +} + +.saliency-label { + height: 16px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + border-bottom: 1px solid var(--bg-tertiary); + color: var(--text-secondary); +} + +.saliency-key { + color: var(--text-primary); +} + +.saliency-total { + color: var(--text-muted); +} + +/* Utilities */ +.empty-state { + padding: 1rem; + text-align: center; + color: var(--text-muted); + font-size: 12px; +} + +/* Scrollbars */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: var(--bg-primary); +} + +::-webkit-scrollbar-thumb { + background: var(--border-color); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--text-muted); +} \ No newline at end of file diff --git a/services/minecraft/src/main.ts b/services/minecraft/src/main.ts index c61bb2d35..2b329e59a 100644 --- a/services/minecraft/src/main.ts +++ b/services/minecraft/src/main.ts @@ -13,7 +13,7 @@ import { CognitiveEngine } from './cognitive' import { initBot } from './composables/bot' import { config, initEnv } from './composables/config' import { createNeuriAgent } from './composables/neuri' -import { DebugService } from './debug-server' +import { DebugService } from './debug' import { wrapPlugin } from './libs/mineflayer' import { initLogger, useLogger } from './utils/logger' diff --git a/services/minecraft/src/utils/logger.ts b/services/minecraft/src/utils/logger.ts index 5ea133704..4efa21929 100644 --- a/services/minecraft/src/utils/logger.ts +++ b/services/minecraft/src/utils/logger.ts @@ -1,6 +1,6 @@ import { Format, LogLevel, setGlobalFormat, setGlobalLogLevel, useLogg } from '@guiiai/logg' -import { DebugService } from '../debug-server' +import { DebugService } from '../debug' export type Logger = ReturnType diff --git a/services/minecraft/src/web/dashboard.html b/services/minecraft/src/web/dashboard.html deleted file mode 100644 index 0bc687f72..000000000 --- a/services/minecraft/src/web/dashboard.html +++ /dev/null @@ -1,711 +0,0 @@ - - - - - - - Minecraft Service Debugger - - - - -
-
Minecraft Service Debugger
-
- - Disconnected - -
-
-
- - -
-
-

System Overview

-
-
- -
-
Event Queue (0)
-
-
Queue is empty
-
-
- - -
-
Currently Processing
-
-
Idle
-
-
-
-
- -
-
-

Blackboard State

-
-
-
{}
-
-
- -
-
- - -
-
-
- -
-
- -
-
-
- -
-
-

Saliency Window

-
- Top - - Slot - 0 -
-
-
-
-
- -
Columns: oldest → newest (100 slots). Rows: counter keys (top-N by total).
-
-
-
Keys
-
-
-
-
-
-
- - - - - \ No newline at end of file