refactor(minecraft): dead code cleanup

Strips saliency thresholds, window counters, signal generation, and route filtering from all perception event definitions and EventRegistry. Events now emit raw data directly via onRawEvent callback without conscious/reflex/debug routing or threshold-based filtering. Removes WindowCounter, SaliencySnapshot, and related timer/slot logic.
This commit is contained in:
Rin
2026-02-18 11:14:45 +08:00
committed by Neko Ayaka
parent 46b1a8d438
commit ae297e8b06
27 changed files with 7 additions and 1806 deletions
@@ -34,22 +34,4 @@ export const armSwingEvent = definePerceptionEvent<[any], ArmSwingExtract>({
}),
},
saliency: {
threshold: 5,
key: 'punch:player',
},
signal: {
type: 'entity_attention',
description: extracted => `Player ${extracted.displayName || 'unknown'} is punching nearby`,
metadata: extracted => ({
kind: 'player',
action: 'punch',
distance: extracted.distance,
hasLineOfSight: extracted.hasLineOfSight,
displayName: extracted.displayName,
}),
},
routes: ['conscious', 'reflex', 'debug'],
})
@@ -86,23 +86,6 @@ function inferCauseFromName(name: string): DamageSourceCause {
return 'unknown'
}
function describeDamageTaken(extracted: DamageTakenExtract): string {
const amount = extracted.amount
const ds = extracted.damageSource
const cause = ds?.cause ?? 'unknown'
const name = ds?.name
const distance = ds?.distance
const details = [
`amount=${amount}`,
`cause=${cause}`,
typeof name === 'string' && name.length > 0 ? `name=${name}` : null,
typeof distance === 'number' ? `distance=${distance.toFixed(1)}` : null,
].filter(Boolean).join(', ')
return `Taken damage (${details}).`
}
export const damageTakenEvent = definePerceptionEvent<[], DamageTakenExtract>({
id: 'damage_taken',
modality: 'felt',
@@ -139,21 +122,4 @@ export const damageTakenEvent = definePerceptionEvent<[], DamageTakenExtract>({
},
},
saliency: {
threshold: 1,
key: 'felt:damage',
},
signal: {
type: 'saliency_high',
description: extracted => describeDamageTaken(extracted),
metadata: extracted => ({
kind: 'felt',
action: 'damage',
amount: extracted.amount,
damageSource: extracted.damageSource,
}),
},
routes: ['conscious', 'reflex', 'debug'],
})
@@ -34,22 +34,4 @@ export const entityMovedEvent = definePerceptionEvent<[any], EntityMovedExtract>
}),
},
saliency: {
threshold: 5,
key: 'move:player',
},
signal: {
type: 'entity_attention',
description: extracted => `Player ${extracted.displayName || 'unknown'} is moving nearby`,
metadata: extracted => ({
kind: 'player',
action: 'move',
distance: extracted.distance,
hasLineOfSight: extracted.hasLineOfSight,
displayName: extracted.displayName,
}),
},
routes: ['reflex', 'debug'],
})
@@ -21,56 +21,4 @@ export const itemCollectedEvent = definePerceptionEvent<[any, any], ItemCollecte
}),
},
saliency: {
threshold: 3,
key: 'felt:pickup',
},
signal: {
type: 'entity_attention',
description: () => 'Picked up an item',
metadata: extracted => ({
kind: 'felt',
action: 'pickup',
itemName: extracted.itemName,
}),
},
routes: ['conscious', 'debug'],
})
// TODO: find out if this is needed. Some servers might use entityCollect instead of playerCollect?
// export const itemCollectedEntityCollectEvent = definePerceptionEvent<[any, any], ItemCollectedExtract>({
// id: 'item_collected_entity_collect',
// modality: 'felt',
// kind: 'item_collected',
// mineflayer: {
// event: 'entityCollect',
// filter: (ctx, collector, _collected) => {
// if (!collector)
// return false
// return collector.username === ctx.selfUsername
// },
// extract: (_ctx, _collector, collected) => ({
// itemName: String(collected?.name ?? collected?.displayName ?? collected?.type ?? 'unknown'),
// }),
// },
// saliency: {
// threshold: 3,
// key: 'felt:pickup',
// },
// signal: {
// type: 'entity_attention',
// description: () => 'Picked up an item',
// metadata: extracted => ({
// kind: 'felt',
// action: 'pickup',
// itemName: extracted.itemName,
// }),
// },
// routes: ['conscious', 'debug'],
// })
@@ -59,22 +59,4 @@ export const sneakToggleEvent = definePerceptionEvent<[any], SneakToggleExtract>
}),
},
saliency: {
threshold: 5,
key: 'teabag:player',
},
signal: {
type: 'entity_attention',
description: extracted => `Player ${extracted.displayName || 'unknown'} is teabagging (rapid sneaking)`,
metadata: extracted => ({
kind: 'player',
action: 'teabag',
distance: extracted.distance,
hasLineOfSight: extracted.hasLineOfSight,
displayName: extracted.displayName,
}),
},
routes: ['conscious', 'reflex', 'debug'],
})
@@ -28,21 +28,4 @@ export const soundHeardEvent = definePerceptionEvent<[string, Vec3], SoundHeardE
}),
},
saliency: {
threshold: 5,
key: 'sound:ambient',
},
signal: {
type: 'environmental_anomaly',
description: extracted => `Heard sound: ${extracted.soundId}`,
metadata: extracted => ({
kind: 'sound',
action: 'sound',
soundId: extracted.soundId,
distance: extracted.distance,
}),
},
routes: ['conscious', 'debug'],
})
@@ -11,19 +11,4 @@ export const systemMessageEvent = definePerceptionEvent<[string, string], { mess
extract: (_ctx, message, position) => ({ message, position }),
},
saliency: {
threshold: 1,
key: 'system:message',
},
signal: {
type: 'system_message',
description: extracted => extracted.message,
metadata: extracted => ({
message: extracted.message,
position: extracted.position,
}),
},
routes: ['reflex'],
})
@@ -5,7 +5,6 @@ import type { Vec3 } from 'vec3'
import type {
PerceptionContext,
PerceptionEventDefinition,
PerceptionSignal,
RawPerceptionEventBase,
} from './types'
@@ -15,17 +14,6 @@ export function definePerceptionEvent<TArgs extends any[], TExtract>(
return definition
}
interface WindowCounter {
head: number
windowSize: number
counts: number[]
triggers: number[]
total: number
lastEventSlot: number
lastFireSlot: number | null
lastFireTotal: number
}
interface RegisteredListener {
event: string
handler: (...args: any[]) => void
@@ -33,33 +21,12 @@ interface RegisteredListener {
export interface EventRegistryDeps {
logger: Logg
onSignal: (signal: PerceptionSignal) => void
onRawEvent?: (event: RawPerceptionEventBase & Record<string, any>) => void
}
const SLOT_MS = 20
const DEFAULT_WINDOW_MS = 2000
export interface CounterSnapshot {
key: string
total: number
window: number[]
triggers: number[]
lastFireSlot: number | null
lastFireTotal: number
}
export interface SaliencySnapshot {
slot: number
counters: CounterSnapshot[]
onRawEvent: (event: RawPerceptionEventBase & Record<string, any>) => void
}
export class EventRegistry {
private definitions: Map<string, PerceptionEventDefinition> = new Map()
private counters: Map<string, WindowCounter> = new Map()
private listeners: RegisteredListener[] = []
private currentSlot = 0
private timer: ReturnType<typeof setInterval> | null = null
private context: PerceptionContext | null = null
private maxDistance = 32
@@ -67,12 +34,6 @@ export class EventRegistry {
public register(definition: PerceptionEventDefinition): void {
this.definitions.set(definition.id, definition)
if (definition.saliency) {
const windowMs = definition.saliency.windowMs ?? DEFAULT_WINDOW_MS
const windowSize = Math.max(1, Math.round(windowMs / SLOT_MS))
this.counters.set(definition.saliency.key, this.createCounter(windowSize))
}
}
public registerAll(definitions: PerceptionEventDefinition[]): void {
@@ -81,47 +42,13 @@ export class EventRegistry {
}
}
public start(): void {
if (this.timer)
return
this.timer = setInterval(() => {
this.currentSlot += 1
this.advanceWindows()
}, SLOT_MS)
}
public stop(): void {
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}
// NOTICE: Nullify context so that any stale mineflayer listeners that fire
// after stop() (but before detachFromBot()) are silently ignored by the
// guard at the top of handleMineflayerEvent.
this.context = null
}
public getDebugSnapshot(): SaliencySnapshot {
const counters: CounterSnapshot[] = []
for (const [key, counter] of this.counters.entries()) {
counters.push({
key,
total: counter.total,
window: this.exportWindow(counter),
triggers: this.exportTriggers(counter),
lastFireSlot: counter.lastFireSlot,
lastFireTotal: counter.lastFireTotal,
})
}
return {
slot: this.currentSlot,
counters,
}
}
public attachToBot(bot: Bot, maxDistance = 32): void {
this.maxDistance = maxDistance
this.context = this.createContext(bot)
@@ -175,22 +102,10 @@ export class EventRegistry {
}
}
public getSignalTypes(): string[] {
const types = new Set<string>()
for (const def of this.definitions.values()) {
types.add(def.signal.type)
}
return Array.from(types)
}
public getDefinitions(): PerceptionEventDefinition[] {
return Array.from(this.definitions.values())
}
public getSaliencyKeys(): string[] {
return Array.from(this.counters.keys())
}
private handleMineflayerEvent(def: PerceptionEventDefinition, args: any[]): void {
if (!this.context)
return
@@ -210,95 +125,7 @@ export class EventRegistry {
...extracted,
}
if (def.routes.includes('reflex'))
this.deps.onRawEvent?.(rawEvent)
if (def.saliency) {
const shouldEmit = this.incrementAndCheck(def.saliency.key, def.saliency.threshold)
if (!shouldEmit)
return
}
const signal: PerceptionSignal = {
type: def.signal.type,
description: def.signal.description(extracted, timestamp),
sourceId: def.id,
confidence: 1.0,
timestamp,
metadata: def.signal.metadata?.(extracted) ?? extracted,
}
if (def.routes.includes('conscious'))
this.deps.onSignal(signal)
}
private incrementAndCheck(key: string, threshold: number): boolean {
const counter = this.counters.get(key)
if (!counter)
return true
counter.counts[counter.head] = (counter.counts[counter.head] ?? 0) + 1
counter.total += 1
counter.lastEventSlot = this.currentSlot
if (counter.total >= threshold) {
counter.lastFireSlot = this.currentSlot
counter.lastFireTotal = counter.total
counter.triggers[counter.head] = 1
this.resetCounter(counter)
return true
}
return false
}
private createCounter(windowSize: number): WindowCounter {
return {
head: 0,
windowSize,
counts: new Array(windowSize).fill(0),
triggers: new Array(windowSize).fill(0),
total: 0,
lastEventSlot: 0,
lastFireSlot: null,
lastFireTotal: 0,
}
}
private resetCounter(counter: WindowCounter): void {
counter.total = 0
counter.counts.fill(0)
// do not reset triggers; they are historical markers for debug
}
private advanceWindows(): void {
for (const counter of this.counters.values()) {
counter.head = (counter.head + 1) % counter.windowSize
const expired = counter.counts[counter.head] ?? 0
if (expired > 0) {
counter.total = Math.max(0, counter.total - expired)
}
counter.counts[counter.head] = 0
counter.triggers[counter.head] = 0
}
}
private exportWindow(counter: WindowCounter): number[] {
const out = new Array<number>(counter.windowSize)
for (let i = 0; i < counter.windowSize; i++) {
const idx = (counter.head + 1 + i) % counter.windowSize
out[i] = counter.counts[idx] ?? 0
}
return out
}
private exportTriggers(counter: WindowCounter): number[] {
const out = new Array<number>(counter.windowSize)
for (let i = 0; i < counter.windowSize; i++) {
const idx = (counter.head + 1 + i) % counter.windowSize
out[i] = counter.triggers[idx] ?? 0
}
return out
this.deps.onRawEvent(rawEvent)
}
}
@@ -3,8 +3,6 @@ import type { Vec3 } from 'vec3'
export type PerceptionModality = 'sighted' | 'heard' | 'felt' | 'system'
export type PerceptionRoute = 'reflex' | 'conscious' | 'debug'
export interface PerceptionContext {
bot: Bot
selfUsername: string
@@ -21,30 +19,12 @@ export interface MineflayerBinding<TArgs extends any[] = any[], TExtract = any>
extract: (ctx: PerceptionContext, ...args: TArgs) => TExtract
}
export interface SaliencyConfig {
threshold: number
key: string
windowMs?: number
}
export interface SignalConfig<TExtract> {
type: string
description: (extracted: TExtract, timestamp: number) => string
metadata?: (extracted: TExtract) => Record<string, any>
}
export interface PerceptionEventDefinition<TArgs extends any[] = any[], TExtract = any> {
id: string
modality: PerceptionModality
kind: string
mineflayer: MineflayerBinding<TArgs, TExtract>
saliency?: SaliencyConfig
signal: SignalConfig<TExtract>
routes: PerceptionRoute[]
}
export interface RawPerceptionEventBase {
@@ -53,12 +33,3 @@ export interface RawPerceptionEventBase {
timestamp: number
source: string
}
export interface PerceptionSignal {
type: string
description: string
sourceId?: string
confidence: number
timestamp: number
metadata: Record<string, any>
}
@@ -1,41 +0,0 @@
import type { RawPerceptionEvent } from './types/raw-events'
export type PerceptionFrameSource = 'minecraft'
export type PerceptionFrameKind
= | 'world_raw'
| 'chat_raw'
export interface PerceptionFrame {
id: string
ts: number
source: PerceptionFrameSource
kind: PerceptionFrameKind
raw: RawPerceptionEvent | { username: string, message: string }
signals: Array<{ type: string, payload: any }>
}
export function createPerceptionFrameFromRawEvent(event: RawPerceptionEvent): PerceptionFrame {
return {
id: `p_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`,
ts: event.timestamp,
source: 'minecraft',
kind: 'world_raw',
raw: event,
signals: [],
}
}
export function createPerceptionFrameFromChat(username: string, message: string): PerceptionFrame {
return {
id: `p_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`,
ts: Date.now(),
source: 'minecraft',
kind: 'chat_raw',
raw: {
username,
message,
},
signals: [],
}
}
@@ -1,35 +0,0 @@
export interface LeakyBucketConfig {
capacity: number
leakPerSecond: number
trigger: number
}
export class LeakyBucket { // Shoutout to Makito for this idea
private value = 0
private readonly capacity: number
private readonly leakPerMs: number
private readonly trigger: number
constructor(config: LeakyBucketConfig) {
this.capacity = config.capacity
this.leakPerMs = config.leakPerSecond / 1000
this.trigger = config.trigger
}
public tick(deltaMs: number): void {
if (deltaMs <= 0)
return
this.value = Math.max(0, this.value - this.leakPerMs * deltaMs)
}
public add(amount: number): { fired: boolean, value: number } {
const next = Math.min(this.capacity, this.value + amount)
const fired = this.value < this.trigger && next >= this.trigger
this.value = next
return { fired, value: this.value }
}
public getValue(): number {
return this.value
}
}
@@ -1,442 +0,0 @@
import type { Logg } from '@guiiai/logg'
import type { Vec3 } from 'vec3'
import type { MineflayerWithAgents } from '../types'
import type {
FeltDamageTakenEvent,
FeltItemCollectedEvent,
HeardSoundEvent,
PlayerJoinedEvent,
RawPerceptionEvent,
SightedArmSwingEvent,
SightedEntityMovedEvent,
SightedSneakToggleEvent,
SystemMessageEvent,
} from './types/raw-events'
export class MineflayerPerceptionCollector {
private bot: MineflayerWithAgents | null = null
private readonly listeners: Array<{
event: string
handler: (...args: any[]) => void
}> = []
private knownPlayerIds: Set<string> = new Set()
private lastSelfHealth: number | null = null
private lastStatsAt = 0
private stats: Record<string, number> = {}
private sneakingState: Map<string, boolean> = new Map()
constructor(
private readonly deps: {
logger: Logg
emitRaw: (event: RawPerceptionEvent) => void
maxDistance: number
},
) { }
public init(bot: MineflayerWithAgents): void {
this.bot = bot
this.lastSelfHealth = bot.bot.health
this.lastStatsAt = Date.now()
this.stats = {}
this.knownPlayerIds = this.snapshotKnownPlayers(bot)
this.deps.logger.withFields({ maxDistance: this.deps.maxDistance }).log('MineflayerPerceptionCollector: init')
this.registerEventHandlers()
}
public destroy(): void {
if (!this.bot)
return
this.deps.logger.withFields({ listeners: this.listeners.length }).log('MineflayerPerceptionCollector: destroy')
for (const { event, handler } of this.listeners) {
try {
const b = this.bot.bot as any
b.off?.(event, handler)
b.removeListener?.(event, handler)
}
catch (err) {
this.deps.logger.withError(err as Error).error('MineflayerPerceptionCollector: failed to remove listener')
}
}
this.listeners.length = 0
this.lastSelfHealth = null
this.bot = null
}
// ========================================
// Event Handler Registration
// ========================================
private registerEventHandlers(): void {
// NOTICE: Sighted events (entityMoved, entitySwingArm, entityUpdate) now handled by EventRegistry
// See perception/events/definitions/arm-swing.ts, sneak-toggle.ts, entity-moved.ts
// NOTICE: Heard/Felt events (soundEffectHeard, health, playerCollect) now handled by EventRegistry
// See perception/events/definitions/sound-heard.ts, damage-taken.ts, item-collected.ts
// NOTICE: playerJoined hook disconnected - system messages now cover join/leave events
// this.onBot('playerJoined', player => this.handlePlayerJoined(player))
// this.onBot('playerUpdated', () => this.handlePlayersMaybeChanged())
// NOTICE: messagestr now handled by EventRegistry in perception/events/definitions/system-message.ts
}
// ========================================
// Sighted Event Handlers
// NOTICE: These handlers are now handled by EventRegistry
// Kept for reference during migration
// ========================================
// @ts-expect-error Intentionally unused - migrated to EventRegistry
private handleEntityMoved(entity: any): void {
if (!this.isValidEntityInRange(entity))
return
const event: SightedEntityMovedEvent = {
modality: 'sighted',
kind: 'entity_moved',
entityType: entity?.type === 'player' ? 'player' : 'mob',
entityId: this.entityId(entity),
displayName: entity?.username,
distance: this.distanceTo(entity)!,
hasLineOfSight: true,
timestamp: Date.now(),
source: 'minecraft',
pos: entity?.position,
}
this.emitEvent(event, 'sighted.entity_moved')
}
// @ts-expect-error Intentionally unused - migrated to EventRegistry
private handleEntitySwingArm(entity: any): void {
if (!this.isValidEntityInRange(entity))
return
const event: SightedArmSwingEvent = {
modality: 'sighted',
kind: 'arm_swing',
entityType: 'player',
entityId: this.entityId(entity),
displayName: entity?.username,
distance: this.distanceTo(entity)!,
hasLineOfSight: true,
timestamp: Date.now(),
source: 'minecraft',
pos: entity?.position,
}
this.emitEvent(event, 'sighted.arm_swing')
}
// @ts-expect-error Intentionally unused - migrated to EventRegistry
private handleEntityUpdate(entity: any): void {
if (!entity || entity.type !== 'player')
return
if (this.isSelfEntity(entity))
return
const entityId = this.entityId(entity)
const isSneaking = this.extractSneakingState(entity)
if (!this.hasSneakingStateChanged(entityId, isSneaking))
return
this.sneakingState.set(entityId, isSneaking)
const dist = this.distanceTo(entity)
if (dist === null || dist > this.deps.maxDistance)
return
const event: SightedSneakToggleEvent = {
modality: 'sighted',
kind: 'sneak_toggle',
entityType: 'player',
entityId,
displayName: entity?.username,
distance: dist,
hasLineOfSight: true,
sneaking: isSneaking,
timestamp: Date.now(),
source: 'minecraft',
pos: entity?.position,
}
this.emitEvent(event, 'sighted.sneak_toggle')
}
// ========================================
// Heard Event Handlers
// NOTICE: These handlers are now handled by EventRegistry
// ========================================
// @ts-expect-error Intentionally unused - migrated to EventRegistry
private handleSoundHeard(soundId: string, pos: Vec3): void {
if (!pos)
return
const dist = this.distanceToPos(pos)
if (dist === null || dist > this.deps.maxDistance)
return
const event: HeardSoundEvent = {
modality: 'heard',
kind: 'sound',
soundId,
distance: dist,
timestamp: Date.now(),
source: 'minecraft',
pos,
}
this.deps.emitRaw(event)
}
// ========================================
// Felt Event Handlers
// NOTICE: These handlers are now handled by EventRegistry
// ========================================
// @ts-expect-error Intentionally unused - migrated to EventRegistry
private handleHealthChange(): void {
if (!this.bot)
return
const current = this.bot.bot.health
const prev = this.lastSelfHealth
this.lastSelfHealth = current
if (typeof prev !== 'number' || current >= prev)
return
const event: FeltDamageTakenEvent = {
modality: 'felt',
kind: 'damage_taken',
amount: prev - current,
timestamp: Date.now(),
source: 'minecraft',
}
this.emitEvent(event, 'felt.damage_taken')
}
// @ts-expect-error Intentionally unused - migrated to EventRegistry
private handleItemCollected(collector: any, collected: any): void {
if (!this.bot || !collector)
return
if (collector.username !== this.bot.bot.username)
return
const itemName = String(collected?.name ?? collected?.displayName ?? collected?.type ?? 'unknown')
const event: FeltItemCollectedEvent = {
modality: 'felt',
kind: 'item_collected',
itemName,
timestamp: Date.now(),
source: 'minecraft',
}
this.emitEvent(event, 'felt.item_collected')
}
// ========================================
// System Event Handlers
// ========================================
// NOTICE: handleSystemMessage now handled by EventRegistry
// @ts-expect-error Intentionally unused - kept for reference during migration
private handleSystemMessage(message: string, messagePosition: string): void {
// messagePosition: 'chat' = player chat, 'system' = system message, 'game_info' = action bar
if (messagePosition !== 'system')
return
const event: SystemMessageEvent = {
modality: 'system',
kind: 'system_message',
message,
position: messagePosition,
timestamp: Date.now(),
source: 'minecraft',
}
this.emitEvent(event, 'system.system_message')
}
// NOTICE: handlePlayerJoined and handlePlayersMaybeChanged are disconnected
// System messages now cover join/leave events via messagestr
// @ts-expect-error Intentionally unused - kept for potential future reconnection
private handlePlayerJoined(player: any): void {
if (!player)
return
if (player.username === this.bot?.bot.username)
return
const playerId = String(player.uuid ?? player.id ?? player.username ?? 'unknown')
if (this.knownPlayerIds.has(playerId))
return
this.knownPlayerIds.add(playerId)
const event: PlayerJoinedEvent = {
modality: 'system',
kind: 'player_joined',
playerId,
displayName: player.username,
timestamp: Date.now(),
source: 'minecraft',
}
this.emitEvent(event, 'system.player_joined')
}
// @ts-expect-error Intentionally unused - kept for potential future reconnection
private handlePlayersMaybeChanged(): void {
const bot = this.bot
if (!bot)
return
const current = this.snapshotKnownPlayers(bot)
for (const playerId of current) {
if (this.knownPlayerIds.has(playerId))
continue
this.knownPlayerIds.add(playerId)
const player = bot.bot.players?.[playerId]
const username = player?.username
const event: PlayerJoinedEvent = {
modality: 'system',
kind: 'player_joined',
playerId,
displayName: typeof username === 'string' ? username : undefined,
timestamp: Date.now(),
source: 'minecraft',
}
this.emitEvent(event, 'system.player_joined')
}
}
private snapshotKnownPlayers(bot: MineflayerWithAgents): Set<string> {
const out = new Set<string>()
const players = bot.bot.players as Record<string, any> | undefined
if (!players)
return out
const selfUsername = bot.bot.username
for (const [id, player] of Object.entries(players)) {
if (!id)
continue
const username = player?.username
if (username && username === selfUsername)
continue
out.add(String(id))
}
return out
}
// ========================================
// Validation Helpers
// ========================================
private isValidEntityInRange(entity: any): boolean {
const dist = this.distanceTo(entity)
if (dist === null || dist > this.deps.maxDistance)
return false
if (this.isSelfEntity(entity))
return false
return true
}
private isSelfEntity(entity: any): boolean {
return entity.username === this.bot?.bot.username
}
private extractSneakingState(entity: any): boolean {
const flags = entity?.metadata?.[0]
// Bit 1 (0x02) is sneaking
return typeof flags === 'number' ? !!(flags & 0x02) : false
}
private hasSneakingStateChanged(entityId: string, isSneaking: boolean): boolean {
const lastState = this.sneakingState.get(entityId)
return lastState !== isSneaking
}
// ========================================
// Utilities
// ========================================
private emitEvent(event: RawPerceptionEvent, statKey: string): void {
this.deps.emitRaw(event)
this.bumpStat(statKey)
this.maybeLogStats()
}
private bumpStat(key: string): void {
this.stats[key] = (this.stats[key] ?? 0) + 1
}
private maybeLogStats(): void {
const now = Date.now()
if (now - this.lastStatsAt < 2000)
return
// this.deps.logger.withFields({
// ...this.stats,
// }).log('MineflayerPerceptionCollector: stats')
this.lastStatsAt = now
this.stats = {}
}
// @ts-expect-error Intentionally unused - all events migrated to EventRegistry
private onBot(event: string, handler: (...args: any[]) => void): void {
if (!this.bot)
return
(this.bot.bot as any).on(event, handler)
this.listeners.push({ event, handler })
}
private entityId(entity: any): string {
return String(entity?.id ?? entity?.uuid ?? entity?.username ?? 'unknown')
}
private distanceTo(entity: any): number | null {
const pos = entity?.position
if (!pos)
return null
return this.distanceToPos(pos)
}
private distanceToPos(pos: Vec3): number | null {
if (!this.bot)
return null
const selfPos = this.bot.bot.entity?.position
if (!selfPos)
return null
try {
return selfPos.distanceTo(pos)
}
catch {
return null
}
}
}
@@ -2,11 +2,7 @@ import type { Logg } from '@guiiai/logg'
import type { EventBus } from '../os'
import type { MineflayerWithAgents } from '../types'
import type { PerceptionFrame } from './frame'
import type { RawPerceptionEvent } from './types/raw-events'
import type { PerceptionStage } from './types/stage'
import { DebugService } from '../../debug'
import { EventRegistry } from './events'
import { allEventDefinitions } from './events/definitions'
import { PerceptionAPI } from './perception-api'
@@ -15,11 +11,6 @@ export class PerceptionPipeline {
private readonly perception: PerceptionAPI
private readonly eventRegistry: EventRegistry
private bot: MineflayerWithAgents | null = null
private initialized = false
private readonly stages: PerceptionStage[]
private saliencyEmitTimer: ReturnType<typeof setInterval> | null = null
constructor(
private readonly deps: {
@@ -31,12 +22,6 @@ export class PerceptionPipeline {
this.eventRegistry = new EventRegistry({
logger: this.deps.logger,
onSignal: (signal) => {
// NOTICE: In the linear architecture, Perception does not emit signal:* directly.
// Raw events are emitted and the rules layer derives signals. Reflex then forwards
// selected signals to conscious.
void signal
},
onRawEvent: (event) => {
const eventType = `raw:${event.modality}:${event.kind}`
this.deps.eventBus.emit({
@@ -47,63 +32,14 @@ export class PerceptionPipeline {
},
})
this.eventRegistry.registerAll(allEventDefinitions)
this.stages = [
{
name: 'entity_update',
handle: (frame) => {
if (frame.kind !== 'world_raw')
return frame
const raw = frame.raw as RawPerceptionEvent
// Feed entity updates to PerceptionAPI
if ('entityId' in raw && 'entityType' in raw) {
const entityRaw = raw as RawPerceptionEvent & { entityId: string, entityType: string, displayName?: string, pos?: { x: number, y: number, z: number } }
if (entityRaw.entityType === 'player') {
this.perception.updateEntity(entityRaw.entityId, {
id: entityRaw.entityId,
type: 'player',
name: entityRaw.displayName,
position: entityRaw.pos as any,
isSneaking: 'sneaking' in entityRaw ? (entityRaw as any).sneaking : undefined,
})
}
}
return frame
},
},
{
name: 'attention',
handle: (frame) => {
if (frame.kind !== 'world_raw')
return frame
const raw = frame.raw as RawPerceptionEvent
// Legacy pipeline saliency disabled; EventRegistry is the source of truth.
// Keep raw emission for now in case some other component still ingests frames.
this.emitRawToEventBus(raw)
return frame
},
},
]
}
public init(bot: MineflayerWithAgents): void {
this.initialized = true
this.bot = bot
this.deps.logger.withFields({ maxDistance: 32 }).log('PerceptionPipeline: init')
this.eventRegistry.start()
this.eventRegistry.attachToBot(bot.bot, 32)
this.saliencyEmitTimer = setInterval(() => {
if (!this.initialized)
return
DebugService.getInstance().emit('saliency', this.eventRegistry.getDebugSnapshot())
}, 100)
}
public destroy(): void {
@@ -114,12 +50,6 @@ export class PerceptionPipeline {
}
this.eventRegistry.stop()
this.bot = null
if (this.saliencyEmitTimer) {
clearInterval(this.saliencyEmitTimer)
this.saliencyEmitTimer = null
}
this.initialized = false
}
/**
@@ -128,47 +58,4 @@ export class PerceptionPipeline {
public getPerceptionAPI(): PerceptionAPI {
return this.perception
}
/**
* Get all registered signal types from the EventRegistry
* Used by consumers (e.g., Brain) to dynamically subscribe to signals
*/
public getSignalTypes(): string[] {
return this.eventRegistry.getSignalTypes()
}
public ingest(frame: PerceptionFrame): void {
if (!this.initialized)
return
let current: PerceptionFrame | null = frame
for (const stage of this.stages) {
if (!current)
break
try {
current = stage.handle(current)
}
catch (err) {
this.deps.logger.withError(err as Error).error('PerceptionPipeline: stage error')
break
}
}
}
/**
* Emit a raw perception event to the EventBus
* This bridges the perception system to the rules layer
*/
private emitRawToEventBus(raw: RawPerceptionEvent): void {
const eventType = `raw:${raw.modality}:${raw.kind}`
this.deps.eventBus.emit({
type: eventType,
payload: Object.freeze(raw),
source: {
component: 'perception',
id: raw.source,
},
})
}
}
@@ -1,25 +0,0 @@
import type { PerceptionFrame } from './frame'
export class RawEventBuffer {
private queue: PerceptionFrame[] = []
public push(event: PerceptionFrame): void {
this.queue.push(event)
}
public size(): number {
return this.queue.length
}
public drain(): PerceptionFrame[] {
if (this.queue.length === 0)
return []
const drained = this.queue
this.queue = []
return drained
}
public clear(): void {
this.queue = []
}
}
@@ -1 +0,0 @@
export * from './types/raw-events'
@@ -1,243 +0,0 @@
import type { Logg } from '@guiiai/logg'
import type { SaliencyRuleBook } from './saliency-rules'
import type { RawPerceptionEvent } from './types/raw-events'
import type { PerceptionSignal } from './types/signals'
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
}
/**
* 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<string, WindowCounter> = new Map()
/** Current slot (advances every slotMs) */
private currentSlot = 0
/** Timer for advancing slots */
private timer: ReturnType<typeof setInterval> | null = null
/** Milliseconds per slot */
private readonly slotMs = 20
constructor(
private readonly deps: {
logger: Logg
onAttention: (signal: PerceptionSignal) => void
rules?: SaliencyRuleBook
},
) {
// 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)
return
this.timer = setInterval(() => {
this.currentSlot += 1
this.advanceWindows()
}, this.slotMs)
}
public stop(): void {
if (!this.timer)
return
clearInterval(this.timer)
this.timer = null
}
/**
* Process an incoming perception event
*/
public ingest(event: RawPerceptionEvent): void {
const rule = this.lookupRule(event)
if (!rule)
return
if (rule.predicate && !rule.predicate(event))
return
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 = this.currentSlot
// Check threshold
if (counter.total >= rule.threshold) {
counter.lastFireSlot = this.currentSlot
counter.lastFireTotal = counter.total
counter.triggers[counter.head] = 1
this.resetCounter(counter)
this.emitSignal(rule.buildSignal(event))
}
}
/**
* Get debug snapshot for visualization
* Returns all counters in fixed order (always same keys, same order)
*/
public getDebugSnapshot(): SaliencySnapshot {
const counters: CounterSnapshot[] = []
for (const key of EVENT_KEYS) {
const counter = this.counters.get(key)
if (!counter)
continue
counters.push({
key,
total: counter.total,
window: this.exportWindow(counter),
triggers: this.exportTriggers(counter),
lastFireSlot: counter.lastFireSlot,
lastFireTotal: counter.lastFireTotal,
})
}
return {
slot: this.currentSlot,
counters,
}
}
/**
* Advance all windows by one slot
* Never deletes counters - they persist with zeroed values
*/
private advanceWindows(): void {
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)
}
// Clear the slot for new data
counter.counts[counter.head] = 0
counter.triggers[counter.head] = 0
}
}
/**
* Create a new counter with zeroed circular buffer
*/
private createCounter(): WindowCounter {
return {
head: 0,
counts: new Array(WINDOW_SIZE).fill(0),
triggers: new Array(WINDOW_SIZE).fill(0),
total: 0,
lastEventSlot: 0,
lastFireSlot: null,
lastFireTotal: 0,
}
}
/**
* Reset counter counts (after threshold triggered)
* Note: We only reset the counts, not triggers - triggers are visual markers
* that should persist until they naturally expire in the circular buffer
*/
private resetCounter(counter: WindowCounter): void {
counter.total = 0
counter.counts.fill(0)
// Don't reset triggers - they're historical markers for visualization
}
/**
* Export window data in chronological order (oldest -> newest)
*/
private exportWindow(counter: WindowCounter): number[] {
const out = new Array<number>(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 out = new Array<number>(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 {
// this.deps.logger.withFields({
// type: signal.type,
// desc: signal.description,
// meta: signal.metadata,
// }).log('SaliencyDetector: emit')
this.deps.onAttention(signal)
}
}
@@ -1,211 +0,0 @@
import type { RawPerceptionEvent } from './types/raw-events'
import type { PerceptionSignal } from './types/signals'
export interface SaliencyRule<E extends RawPerceptionEvent = RawPerceptionEvent> {
/**
* 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<Record<
RawPerceptionEvent['modality'],
Record<string, SaliencyRule>
>>
/**
* 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',
'system:message',
] 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: {
threshold: 5,
key: 'punch:player',
buildSignal: (event) => {
const e = event as Extract<RawPerceptionEvent, { modality: 'sighted', kind: 'arm_swing' }>
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: {
threshold: 5,
key: 'teabag:player',
buildSignal: (event) => {
const e = event as Extract<RawPerceptionEvent, { modality: 'sighted', kind: 'sneak_toggle' }>
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: {
threshold: 5,
predicate: event => (event as Extract<RawPerceptionEvent, { modality: 'sighted', kind: 'entity_moved' }>).entityType === 'player',
key: 'move:player',
buildSignal: (event) => {
const e = event as Extract<RawPerceptionEvent, { modality: 'sighted', kind: 'entity_moved' }>
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<RawPerceptionEvent, { modality: 'heard', kind: 'sound' }>
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: (event) => {
const e = event as Extract<RawPerceptionEvent, { modality: 'felt', kind: 'damage_taken' }>
const ds = (e as any).damageSource as any
const cause = typeof ds?.cause === 'string' ? ds.cause : 'unknown'
const name = typeof ds?.name === 'string' && ds.name.length > 0 ? ds.name : undefined
const distance = typeof ds?.distance === 'number' ? ds.distance : undefined
const amount = typeof (e as any).amount === 'number' ? (e as any).amount : undefined
const details = [
amount !== undefined ? `amount=${amount}` : null,
`cause=${cause}`,
name ? `name=${name}` : null,
distance !== undefined ? `distance=${distance.toFixed(1)}` : null,
].filter(Boolean).join(', ')
return {
type: 'saliency_high',
description: `Taken damage (${details}).`,
confidence: 1.0,
timestamp: Date.now(),
metadata: {
kind: 'felt',
action: 'damage',
amount: e.amount,
damageSource: e.damageSource,
},
}
},
},
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',
},
}),
},
},
system: {
system_message: {
threshold: 1, // System messages are immediately salient
key: 'system:message',
buildSignal: (event) => {
const e = event as Extract<RawPerceptionEvent, { modality: 'system', kind: 'system_message' }>
return {
type: 'system_message',
description: e.message,
sourceId: 'system',
confidence: 1.0,
timestamp: Date.now(),
metadata: {
message: e.message,
position: e.position,
},
}
},
},
},
}
@@ -1 +0,0 @@
export * from './types/stage'
@@ -1,65 +1,3 @@
import type { Vec3 } from 'vec3'
export type PerceptionModality = 'sighted' | 'heard' | 'felt' | 'system'
export interface RawPerceptionEventBase {
modality: PerceptionModality
timestamp: number
source: 'minecraft'
pos?: Vec3
}
export interface SightedEntityMovedEvent extends RawPerceptionEventBase {
modality: 'sighted'
kind: 'entity_moved'
entityType: 'player' | 'mob'
entityId: string
displayName?: string
distance: number
hasLineOfSight: boolean
}
export interface SightedArmSwingEvent extends RawPerceptionEventBase {
modality: 'sighted'
kind: 'arm_swing'
entityType: 'player'
entityId: string
displayName?: string
distance: number
hasLineOfSight: boolean
}
export interface SightedSneakToggleEvent extends RawPerceptionEventBase {
modality: 'sighted'
kind: 'sneak_toggle'
entityType: 'player'
entityId: string
displayName?: string
distance: number
hasLineOfSight: boolean
sneaking: boolean
}
export type SightedEvent = SightedEntityMovedEvent | SightedArmSwingEvent | SightedSneakToggleEvent
export interface HeardSoundEvent extends RawPerceptionEventBase {
modality: 'heard'
kind: 'sound'
soundId: string
distance: number
inferredEntityType?: 'player' | 'mob'
inferredEntityId?: string
}
export type HeardEvent = HeardSoundEvent
export interface FeltDamageTakenEvent extends RawPerceptionEventBase {
modality: 'felt'
kind: 'damage_taken'
amount?: number
damageSource?: DamageSourceMetadata
}
export type DamageSourceCause
= | 'player'
| 'mob'
@@ -79,30 +17,3 @@ export interface DamageSourceMetadata {
entityId?: string
distance?: number
}
export interface FeltItemCollectedEvent extends RawPerceptionEventBase {
modality: 'felt'
kind: 'item_collected'
itemName: string
count?: number
}
export type FeltEvent = FeltDamageTakenEvent | FeltItemCollectedEvent
export interface PlayerJoinedEvent extends RawPerceptionEventBase {
modality: 'system'
kind: 'player_joined'
playerId: string
displayName?: string
}
export interface SystemMessageEvent extends RawPerceptionEventBase {
modality: 'system'
kind: 'system_message'
message: string
position: string
}
export type SystemEvent = PlayerJoinedEvent | SystemMessageEvent
export type RawPerceptionEvent = SightedEvent | HeardEvent | FeltEvent | SystemEvent
@@ -1,9 +1,9 @@
export type PerceptionSignalType
= | 'chat_message'
| 'entity_attention' // e.g. someone waving, teabagging
| 'entity_attention' // e.g. someone waving, punching
| 'environmental_anomaly' // e.g. sudden loud sound
| 'saliency_high' // generic high saliency event
| 'social_gesture' // e.g. teabagging, waving
| 'saliency_high' // generic high-priority event (e.g. damage)
| 'social_gesture' // e.g. waving
| 'social_presence'
| 'system_message' // e.g. death messages, join/leave
@@ -1,7 +0,0 @@
import type { PerceptionFrame } from '../frame'
export interface PerceptionStage {
name: string
tick?: (deltaMs: number) => void
handle: (frame: PerceptionFrame) => PerceptionFrame | null
}
@@ -9,7 +9,6 @@ import type {
ReflexStateEvent,
ReplExecutionResultEvent,
ReplStateEvent,
SaliencyEvent,
ServerEvent,
TraceEvent,
} from './types'
@@ -116,24 +115,6 @@ export class DebugService {
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)
}
/**
* Emit a single trace event from the EventBus
*/
@@ -212,19 +193,6 @@ export class DebugService {
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
}
case 'reflex':
this.emitReflexState(payload as Omit<ReflexStateEvent, 'timestamp'>)
break
-1
View File
@@ -8,6 +8,5 @@ export type {
LLMTraceEvent,
LogEvent,
QueueEvent,
SaliencyEvent,
ServerEvent,
} from './types'
-14
View File
@@ -55,19 +55,6 @@ export interface QueueEvent {
timestamp: number
}
export interface SaliencyEvent {
slot: number
counters: Array<{
key: string
total: number
window: number[]
triggers: number[]
lastFireSlot: number | null
lastFireTotal: number
}>
timestamp: number
}
/**
* Reflex system state update
*/
@@ -189,7 +176,6 @@ export type ServerEvent
| { type: 'llm', payload: LLMTraceEvent }
| { type: 'blackboard', payload: BlackboardEvent }
| { type: 'queue', payload: QueueEvent }
| { type: 'saliency', payload: SaliencyEvent }
| { type: 'reflex', payload: ReflexStateEvent }
| { type: 'trace', payload: TraceEvent }
| { type: 'trace_batch', payload: TraceBatchEvent }
-104
View File
@@ -731,107 +731,6 @@ class ConversationPanel {
}
}
// =============================================================================
// 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 = '<div class="empty-state">No data</div>'
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 => `
<div class="saliency-label">
<span class="saliency-key">${escapeHtml(c.key || '')}</span>
<span class="saliency-total">(${c.total || 0})</span>
</div>
`).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) {
// Always show a subtle color for empty cells, never fully transparent
if (value === 0) {
return 'rgba(30, 38, 50, 1)' // Dark background for empty cells
}
// Normalize value when we have data
const max = maxValue || 1
const t = Math.min(1, value / max)
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.3 + 0.7 * t})`
}
}
// =============================================================================
// Tools Panel
// =============================================================================
@@ -1491,7 +1390,6 @@ class LayoutManager {
{ id: 'splitter-v1', var: '--col-left', type: 'v' },
{ id: 'splitter-h1', var: '--row-1', type: 'h' },
{ id: 'splitter-h2', var: '--row-2', type: 'h' },
{ id: 'splitter-h3', var: '--row-3', type: 'h' },
]
splitters.forEach((config) => {
@@ -1578,7 +1476,6 @@ class DebugApp {
this.brainPanel = new BrainPanel(this.client)
this.logsPanel = new LogsPanel(this.client)
this.conversationPanel = new ConversationPanel(this.client)
this.saliencyPanel = new SaliencyPanel(this.client)
this.timelinePanel = new TimelinePanel(this.client)
this.toolsPanel = new ToolsPanel(this.client)
this.replPanel = new ReplPanel(this.client)
@@ -1589,7 +1486,6 @@ class DebugApp {
brain: this.brainPanel,
logs: this.logsPanel,
conversation: this.conversationPanel,
saliency: this.saliencyPanel,
timeline: this.timelinePanel,
tools: this.toolsPanel,
repl: this.replPanel,
@@ -207,25 +207,6 @@
</div>
</section>
<!-- Splitter H3 (LLM vs Saliency) -->
<div class="splitter-h" id="splitter-h3"></div>
<!-- Bottom Row: Saliency -->
<section id="saliency-section" class="panel">
<div class="panel-header">
<h2>Saliency Heatmap</h2>
<div class="panel-controls">
<span>Slot: <span id="saliency-slot" class="panel-badge">0</span></span>
<button class="icon-btn maximize-btn" title="Toggle maximize"></button>
</div>
</div>
<div class="panel-content">
<div class="saliency-container">
<canvas id="saliency-canvas"></canvas>
<div id="saliency-labels" class="saliency-labels"></div>
</div>
</div>
</section>
</div>
</div>
</main>
+2 -49
View File
@@ -182,9 +182,9 @@ button:hover,
display: grid;
/* Columns: Left Panel | Splitter | Right Content */
grid-template-columns: 1fr;
/* Rows: Logs | Splitter | Timeline | Splitter | LLM | Splitter | Saliency */
/* Rows: Logs | Splitter | Timeline | Splitter | Conversation */
grid-template-rows:
minmax(var(--row-1), 1fr) 8px minmax(var(--row-2), 1fr) 8px minmax(var(--row-3), 1fr) 8px 1fr;
minmax(var(--row-1), 1fr) 8px minmax(var(--row-2), 1fr) 8px minmax(var(--row-3), 1fr);
gap: 0;
padding: 0;
height: 100%;
@@ -455,17 +455,6 @@ button:hover,
min-height: 0;
}
#splitter-h3 {
grid-column: 3 / 4;
grid-row: 6 / 7;
}
#saliency-section {
grid-column: 3 / 4;
grid-row: 7 / 8;
min-height: 0;
}
/* Panel Styles */
.panel {
display: flex;
@@ -834,42 +823,6 @@ button:hover,
40% { transform: translateY(-3px); opacity: 0.9; }
}
/* 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;