v0.15.7: Fix action routing for v4, WS error handling, stable nodes panel

Action routing:
- Button clicks now route through PA→Expert in v4 (was missing has_pa check)
- Previously crashed with KeyError on missing thinker node

WS error handling:
- Exceptions in WS handler caught and logged, not crash
- Frontend receives error HUD event instead of disconnect
- Prevents 1006 reconnect loops on action errors

Nodes panel:
- Fixed pipeline order (no re-sorting on events)
- Deduplicated node names (pa_v1→pa, expert_eras→eras)
- Normalized names in state tracker

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nico 2026-03-29 19:16:15 +02:00
parent 84fa0830d8
commit faeb9d3254
3 changed files with 41 additions and 17 deletions

View File

@ -153,12 +153,20 @@ def register_routes(app):
msg = json.loads(data)
# Always use current runtime (may change after graph switch)
rt = _active_runtime or runtime
if msg.get("type") == "action":
await rt.handle_action(msg.get("action", "unknown"), msg.get("data"))
elif msg.get("type") == "cancel_process":
rt.process_manager.cancel(msg.get("pid", 0))
else:
await rt.handle_message(msg.get("text", ""), dashboard=msg.get("dashboard"))
try:
if msg.get("type") == "action":
await rt.handle_action(msg.get("action", "unknown"), msg.get("data"))
elif msg.get("type") == "cancel_process":
rt.process_manager.cancel(msg.get("pid", 0))
else:
await rt.handle_message(msg.get("text", ""), dashboard=msg.get("dashboard"))
except Exception as e:
import traceback
log.error(f"[ws] handler error: {e}\n{traceback.format_exc()}")
try:
await ws.send_text(json.dumps({"type": "hud", "node": "runtime", "event": "error", "detail": str(e)[:200]}))
except Exception:
pass
except WebSocketDisconnect:
if _active_runtime:
_active_runtime.detach_ws()

View File

@ -523,7 +523,7 @@ class FrameEngine:
return self._make_result(result)
# Complex action — needs full pipeline
self._end_frame(rec, output_summary="no local handler", route="director/thinker")
self._end_frame(rec, output_summary="no local handler", route="pa/director/thinker")
action_desc = f"ACTION: {action}"
if data:
@ -535,7 +535,9 @@ class FrameEngine:
analysis=InputAnalysis(intent="action", topic=action, complexity="simple"),
source_text=action_desc)
if self.has_director:
if self.has_pa:
return await self._run_expert_pipeline(command, mem_ctx, dashboard)
elif self.has_director:
return await self._run_director_pipeline(command, mem_ctx, dashboard)
else:
return await self._run_thinker_pipeline(command, mem_ctx, dashboard)

View File

@ -9,15 +9,21 @@ let _sensorReadings = {};
// --- Node state tracker ---
const _nodeState = {}; // { nodeName: { model, tokens, maxTokens, fillPct, lastEvent, lastDetail, status, toolCalls, startedAt } }
// Normalize node names to avoid duplicates (pa_v1→pa, expert_eras→eras, etc.)
function _normName(name) {
return name.replace('_v1', '').replace('_v2', '').replace('expert_', '');
}
function _getNode(name) {
if (!_nodeState[name]) {
_nodeState[name] = {
const key = _normName(name);
if (!_nodeState[key]) {
_nodeState[key] = {
model: '', tokens: 0, maxTokens: 0, fillPct: 0,
lastEvent: '', lastDetail: '', status: 'idle',
toolCalls: 0, lastTool: '',
};
}
return _nodeState[name];
return _nodeState[key];
}
export function updateNodeFromHud(node, event, data) {
@ -74,15 +80,23 @@ export function updateNodeFromHud(node, event, data) {
renderNodes();
}
// Fixed pipeline order — no re-sorting
// Fixed pipeline order using normalized names
const PIPELINE_ORDER = ['input', 'pa', 'director', 'eras', 'plankiste',
'thinker', 'interpreter', 'output', 'memorizer', 'ui', 'sensor'];
function renderNodes() {
const el = document.getElementById('node-metrics');
if (!el) { console.warn('[nodes] #node-metrics not found'); return; }
if (!el) return;
// Sort: active nodes first, then by name
const statusOrder = { thinking: 0, tool: 0, streaming: 0, planned: 1, done: 2, idle: 3 };
const sorted = Object.entries(_nodeState)
.filter(([name]) => name !== 'runtime' && name !== 'frame_engine')
.sort((a, b) => (statusOrder[a[1].status] || 3) - (statusOrder[b[1].status] || 3));
const entries = Object.entries(_nodeState)
.filter(([name]) => name !== 'runtime' && name !== 'frame_engine');
const sorted = entries.sort((a, b) => {
const ia = PIPELINE_ORDER.indexOf(a[0]);
const ib = PIPELINE_ORDER.indexOf(b[0]);
return (ia === -1 ? 99 : ia) - (ib === -1 ? 99 : ib);
});
let html = '';
for (const [name, n] of sorted) {