fix(minecraft): make logging actually meaningful

Ported from commit f8b73bbf (partial - debug files only):
- Add /api/logs endpoint for listing and loading persisted log files
- Add log file selector UI to debug dashboard
- Filter persistEvent to only save relevant event types
- Add refreshFileList and loadPersistedLog methods to LogsPanel
This commit is contained in:
Rin
2026-02-18 11:14:07 +08:00
committed by Neko Ayaka
parent cb13baf108
commit 02d3f38663
5 changed files with 388 additions and 23 deletions
@@ -0,0 +1,252 @@
# REPL Self-Evolution Roadmap (Discussion Draft)
## Goal
Turn the current JS planner REPL into a "poor man's coding agent" environment that can:
1. Inspect its own runtime and project code.
2. Read docs (especially Mineflayer docs) from within the REPL.
3. Propose and eventually apply tool/runtime improvements safely.
4. Register event hooks and optional background daemons for long-lived behaviors.
No implementation details in this draft are final; this is a planning artifact for review.
---
## Design Principles
- **Safety-first by default**: proposal mode before apply mode.
- **Auditability**: every introspection and mutation action is logged and replayable.
- **Layered capabilities**: observation -> proposal -> gated execution.
- **Deterministic core loop**: keep turn-based planner behavior stable.
- **Minimal trusted surface**: expose small, explicit APIs rather than raw Node/system access.
---
## Capability Areas
## 1) Self-Introspection Interface (REPL APIs)
Expose a read-oriented introspection namespace in REPL globals.
Proposed APIs:
- `introspect.runtime()` -> model, tool list, limits, feature flags, queue status.
- `introspect.tools()` -> current action/tool signatures and schemas.
- `introspect.memory()` -> mem summary + size stats.
- `introspect.last()` -> previous script/action results.
- `introspect.health()` -> basic diagnostics (event lag, retries, failures).
Notes:
- Start read-only.
- Keep outputs compact and structured.
## 2) Project Code Reading Interface
Expose controlled repository browsing from REPL without full shell.
Proposed APIs:
- `repo.list(path = ".", opts?)` -> file/dir listing (allowlisted roots).
- `repo.read(path, opts?)` -> file snippet with line ranges and size cap.
- `repo.search(query, opts?)` -> ripgrep-backed search with result caps.
- `repo.symbol(path, name)` -> optional simple symbol lookup (later phase).
Guardrails:
- Read-only initially.
- Max file size / line range limits.
- Block secrets and ignored paths (`.env`, keys, tokens, node_modules, build outputs).
## 3) Mineflayer Knowledge Surface
Provide docs/refs to help the agent compose better behavior/tool proposals.
Options:
- Local curated docs bundle (recommended for deterministic behavior).
- Indexed snippets for common APIs (movement, inventory, entities, events).
- Optional "doc cards" in prompt context for high-frequency use.
REPL API:
- `docs.find("pathfinder")`
- `docs.read("mineflayer.bot.chat")`
- `docs.example("collectBlock")`
## 4) Self-Evolution Workflow (Proposal First)
Add a governance pipeline before any self-modification:
1. Agent inspects code/docs/runtime.
2. Agent emits a **Change Proposal** object:
- rationale
- files impacted
- risks
- test plan
- rollback plan
3. Human reviews proposal.
4. Optional apply step (future): patch generation + tests + review gate.
Proposed REPL APIs:
- `evolve.propose(spec)`
- `evolve.listProposals()`
- `evolve.getProposal(id)`
- `evolve.reject(id, reason)` / `evolve.approve(id)` (human-mediated)
## 5) Event Hooks
Support persistent declarative hooks that trigger scripts on conditions.
Hook model:
- Trigger: chat/perception/feedback/time tick/custom.
- Condition: JS predicate over `ctx`/`last`/`mem`.
- Action: script body or tool calls.
- Policy: debounce, cooldown, max executions, priority.
API sketch:
- `hooks.register({ name, on, when, script, policy })`
- `hooks.list()`
- `hooks.disable(name)` / `hooks.enable(name)` / `hooks.remove(name)`
Guardrails:
- Prevent recursive storms (hook triggers caused by own outputs).
- Global execution budget per minute.
- Trace each hook invocation.
## 6) Background Daemons
Allow optional long-running tasks for monitoring/planning loops.
Potential uses:
- Patrol/scan loops.
- Inventory housekeeping.
- Threat watcher with alerts.
- Social etiquette responder.
API sketch:
- `daemon.start({ name, script, intervalMs, policy })`
- `daemon.stop(name)`
- `daemon.status(name)` / `daemon.list()`
Constraints:
- Strict per-daemon CPU/time budgets.
- Kill switch and watchdog timeout.
- No direct world mutation unless via validated tool intents.
---
## Architecture Sketch
## REPL Context Modules
- `ctx` (readonly, per-turn injected)
- `last` (readonly, last outcome)
- `mem` (persistent writable)
- `introspect` (readonly API)
- `repo` (read-only at first)
- `docs` (read-only retrieval)
- `hooks` (managed registration)
- `daemon` (managed lifecycle)
- `evolve` (proposal orchestration)
## Runtime Services (outside REPL sandbox)
- Introspection service
- Repo/document index service
- Hook scheduler
- Daemon supervisor
- Proposal registry + audit log
---
## Rollout Plan
## Phase 0 - Foundation Hardening
- Finalize `ctx/last/mem` conventions and docs.
- Add telemetry on script size, execution time, action count.
- Add rate limits and anti-loop protections.
## Phase 1 - Read-Only Introspection + Repo/Docs Read
- Implement `introspect.*`, `repo.list/read/search`, `docs.find/read`.
- Add allowlist + secret/path guards.
- Add prompt conventions for using these APIs responsibly.
## Phase 2 - Proposal-Only Self-Evolution
- Implement `evolve.propose` and proposal registry.
- Define proposal schema + scoring rubric.
- Add review UI/log stream hooks.
## Phase 3 - Hooks + Daemons (Constrained)
- Implement managed hooks with cooldown/budgets.
- Implement supervised daemon runner.
- Add kill switch + watchdog + per-feature flags.
## Phase 4 - Assisted Apply (Optional)
- Agent can prepare patch candidates + test commands.
- Human approval required before applying.
- Automatic rollback metadata and validation report.
---
## Prompt/Policy Conventions to Add
- Prefer world tools for immediate actions.
- Use `repo/docs/introspect` for understanding before proposing changes.
- Never assume write privileges; use `evolve.propose`.
- Keep hook/daemon logic idempotent and bounded.
- Explain intent in `mem` notes before major behavior changes.
---
## Risk Register
- **Runaway autonomy**: hooks/daemons creating infinite loops.
- **Context bloat**: huge introspection outputs inflating token use.
- **Unsafe self-editing**: bad proposals that degrade behavior.
- **Secret leakage**: repo read accidentally exposing credentials.
- **Operational complexity**: too many async subsystems reducing predictability.
Mitigations:
- Hard limits + quotas + cooldowns.
- Strong allowlists and redaction layers.
- Proposal-only first, human gates later.
- Feature flags for every new subsystem.
- Rich tracing and one-command emergency disable.
---
## Open Questions (For Discussion)
1. Should proposal approval live in chat only, or also in a local UI/CLI queue?
2. How much repo scope should be readable by default?
3. Should hooks/daemons be persisted across restarts?
4. Do we want separate "safe mode" and "experimental mode" presets?
5. What exact success metrics define "self-evolution is helping"?
---
## Suggested First Milestone
Implement only:
- `introspect.runtime/tools/health`
- `repo.list/read/search` (read-only, strict limits)
- `docs.find/read` (curated Mineflayer docs)
- `evolve.propose` registry (no apply)
This gives strong value quickly while keeping risk low.
+5 -14
View File
@@ -2,7 +2,6 @@ import type { MineflayerPlugin } from '../libs/mineflayer'
import type { CognitiveEngineOptions, MineflayerWithAgents } from './types'
import { config } from '../composables/config'
import { DebugService } from '../debug'
import { ChatMessageHandler } from '../libs/mineflayer'
import { createAgentContainer } from './container'
import { computeNearbyPlayerGaze } from './reflex/gaze'
@@ -70,21 +69,13 @@ export function CognitiveEngine(options: CognitiveEngineOptions): MineflayerPlug
})
})
// Resolve EventBus and subscribe to forward events to debug timeline
// Resolve EventBus for message handling
const eventBus = container.resolve('eventBus')
eventBus.subscribe('*', (event) => {
// Forward to debug service for timeline visualization
DebugService.getInstance().emitTrace({
id: event.id,
traceId: event.traceId,
parentId: event.parentId,
type: event.type,
payload: event.payload,
timestamp: event.timestamp,
source: event.source,
})
})
// NOTICE: EventBus trace forwarding disabled - trace logs removed to reduce noise
// All events from EventBus were being forwarded to DebugService as trace events,
// causing thousands of 'raw:sighted:entity_moved' entries in the logs.
// Conscious layer (LLM) events are still logged separately.
// Set message handling via EventBus
const chatHandler = new ChatMessageHandler(bot.username)
+69 -7
View File
@@ -183,6 +183,11 @@ export class DebugServer {
return
}
if (req.method === 'GET' && req.url?.startsWith('/api/logs')) {
this.handleLogsApi(req, res)
return
}
if (req.method === 'GET' && (req.url === '/viewer' || req.url?.startsWith('/viewer?'))) {
const html = createViewerHtml('http://localhost:3007')
res.writeHead(200, {
@@ -338,13 +343,19 @@ export class DebugServer {
}
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)
}
const persistableTypes: ServerEvent['type'][] = ['log', 'llm', 'blackboard', 'queue', 'trace', 'trace_batch', 'reflex']
if (!persistableTypes.includes(event.type))
return
if (!this.logStream)
return
try {
this.logStream.write(`${JSON.stringify(event)}\n`)
}
catch (err) {
console.error('Failed to write to log file', err)
}
}
@@ -359,4 +370,55 @@ export class DebugServer {
private generateClientId(): string {
return `client-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
}
private handleLogsApi(req: IncomingMessage, res: http.ServerResponse): void {
const url = new URL(req.url || '/api/logs', 'http://localhost')
const logsDir = path.join(process.cwd(), 'logs')
// GET /api/logs -> list files
if (!url.searchParams.has('file')) {
const files = fs.existsSync(logsDir)
? fs.readdirSync(logsDir)
.filter(f => f.endsWith('.jsonl'))
.sort((a, b) => b.localeCompare(a))
: []
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ files }))
return
}
// GET /api/logs?file=...&limit=500
const fileParam = url.searchParams.get('file') || ''
const safeName = path.basename(fileParam)
const limit = Number.parseInt(url.searchParams.get('limit') || '500', 10)
const lineLimit = Number.isFinite(limit) ? Math.max(1, Math.min(limit, 5000)) : 500
const targetPath = path.join(logsDir, safeName)
if (!fs.existsSync(targetPath)) {
res.writeHead(404, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: 'file not found' }))
return
}
try {
const lines = fs.readFileSync(targetPath, 'utf-8')
.trim()
.split('\n')
.slice(-lineLimit)
const events = lines
.map((line) => {
try { return JSON.parse(line) }
catch { return null }
})
.filter(Boolean)
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ file: safeName, events, total: lines.length }))
}
catch (err) {
res.writeHead(500, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: 'failed to read log file', message: (err as Error).message }))
}
}
}
+58 -2
View File
@@ -412,11 +412,14 @@ class LogsPanel {
this.autoScroll = true
this.paused = false
this.filter = { level: 'all', search: '' }
this.currentFile = ''
this.elements = {
container: document.getElementById('logs-container'),
search: document.getElementById('log-search'),
levelFilter: document.getElementById('log-level-filter'),
autoScroll: document.getElementById('auto-scroll'),
fileSelect: document.getElementById('log-file-select'),
loadFile: document.getElementById('load-log-file'),
statEvents: document.getElementById('stat-events'),
}
}
@@ -439,6 +442,18 @@ class LogsPanel {
this.autoScroll = e.target.checked
})
this.elements.loadFile.addEventListener('click', () => {
const file = this.elements.fileSelect.value
if (!file) {
this.currentFile = ''
this.reset()
return
}
this.loadPersistedLog(file)
})
this.refreshFileList()
this.renderThrottled = throttle(() => this.render(), CONFIG.UPDATE_THROTTLE)
this.render()
}
@@ -462,6 +477,47 @@ class LogsPanel {
this.render()
}
async refreshFileList() {
try {
const res = await fetch('/api/logs')
const json = await res.json()
const files = json.files || []
const select = this.elements.fileSelect
if (!select)
return
const current = select.value
select.innerHTML = '<option value="">Live (current session)</option>' + files.map(f => `<option value="${f}">${f}</option>`).join('')
if (files.includes(current))
select.value = current
}
catch (err) {
console.error('Failed to load log files', err)
}
}
async loadPersistedLog(file) {
try {
const res = await fetch(`/api/logs?file=${encodeURIComponent(file)}&limit=1000`)
if (!res.ok) {
console.error('Failed to fetch log file', await res.text())
return
}
const json = await res.json()
const events = Array.isArray(json.events) ? json.events : []
const logEntries = events
.filter(e => e.type === 'log')
.map(e => e.payload)
.filter(Boolean)
this.logs = logEntries
this.currentFile = file
this.elements.statEvents.textContent = this.logs.length
this.render()
}
catch (err) {
console.error('Failed to load persisted log', err)
}
}
clear() {
this.logs = []
this.elements.statEvents.textContent = '0'
@@ -556,8 +612,8 @@ class LLMPanel {
<div class="llm-message role-${msg.role || 'unknown'}">
<div class="llm-message-role">${msg.role || 'unknown'}</div>
<div class="llm-message-content">${escapeHtml(msg.role === 'system'
? formatSystemMessageContent(msg.content || '')
: (msg.content || ''))}</div>
? formatSystemMessageContent(msg.content || '')
: (msg.content || ''))}</div>
</div>
`).join('')
}
@@ -138,6 +138,10 @@
<option value="INFO">INFO</option>
<option value="DEBUG">DEBUG</option>
</select>
<select id="log-file-select" title="Select persisted log file">
<option value="">Live (current session)</option>
</select>
<button id="load-log-file" class="icon-btn" title="Load selected log file"></button>
<label class="checkbox-label">
<input type="checkbox" id="auto-scroll" checked />
Auto-scroll