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:
parent
84fa0830d8
commit
faeb9d3254
@ -153,12 +153,20 @@ def register_routes(app):
|
|||||||
msg = json.loads(data)
|
msg = json.loads(data)
|
||||||
# Always use current runtime (may change after graph switch)
|
# Always use current runtime (may change after graph switch)
|
||||||
rt = _active_runtime or runtime
|
rt = _active_runtime or runtime
|
||||||
|
try:
|
||||||
if msg.get("type") == "action":
|
if msg.get("type") == "action":
|
||||||
await rt.handle_action(msg.get("action", "unknown"), msg.get("data"))
|
await rt.handle_action(msg.get("action", "unknown"), msg.get("data"))
|
||||||
elif msg.get("type") == "cancel_process":
|
elif msg.get("type") == "cancel_process":
|
||||||
rt.process_manager.cancel(msg.get("pid", 0))
|
rt.process_manager.cancel(msg.get("pid", 0))
|
||||||
else:
|
else:
|
||||||
await rt.handle_message(msg.get("text", ""), dashboard=msg.get("dashboard"))
|
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:
|
except WebSocketDisconnect:
|
||||||
if _active_runtime:
|
if _active_runtime:
|
||||||
_active_runtime.detach_ws()
|
_active_runtime.detach_ws()
|
||||||
|
|||||||
@ -523,7 +523,7 @@ class FrameEngine:
|
|||||||
return self._make_result(result)
|
return self._make_result(result)
|
||||||
|
|
||||||
# Complex action — needs full pipeline
|
# 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}"
|
action_desc = f"ACTION: {action}"
|
||||||
if data:
|
if data:
|
||||||
@ -535,7 +535,9 @@ class FrameEngine:
|
|||||||
analysis=InputAnalysis(intent="action", topic=action, complexity="simple"),
|
analysis=InputAnalysis(intent="action", topic=action, complexity="simple"),
|
||||||
source_text=action_desc)
|
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)
|
return await self._run_director_pipeline(command, mem_ctx, dashboard)
|
||||||
else:
|
else:
|
||||||
return await self._run_thinker_pipeline(command, mem_ctx, dashboard)
|
return await self._run_thinker_pipeline(command, mem_ctx, dashboard)
|
||||||
|
|||||||
@ -9,15 +9,21 @@ let _sensorReadings = {};
|
|||||||
// --- Node state tracker ---
|
// --- Node state tracker ---
|
||||||
const _nodeState = {}; // { nodeName: { model, tokens, maxTokens, fillPct, lastEvent, lastDetail, status, toolCalls, startedAt } }
|
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) {
|
function _getNode(name) {
|
||||||
if (!_nodeState[name]) {
|
const key = _normName(name);
|
||||||
_nodeState[name] = {
|
if (!_nodeState[key]) {
|
||||||
|
_nodeState[key] = {
|
||||||
model: '', tokens: 0, maxTokens: 0, fillPct: 0,
|
model: '', tokens: 0, maxTokens: 0, fillPct: 0,
|
||||||
lastEvent: '', lastDetail: '', status: 'idle',
|
lastEvent: '', lastDetail: '', status: 'idle',
|
||||||
toolCalls: 0, lastTool: '',
|
toolCalls: 0, lastTool: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return _nodeState[name];
|
return _nodeState[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateNodeFromHud(node, event, data) {
|
export function updateNodeFromHud(node, event, data) {
|
||||||
@ -74,15 +80,23 @@ export function updateNodeFromHud(node, event, data) {
|
|||||||
renderNodes();
|
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() {
|
function renderNodes() {
|
||||||
const el = document.getElementById('node-metrics');
|
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 entries = Object.entries(_nodeState)
|
||||||
const statusOrder = { thinking: 0, tool: 0, streaming: 0, planned: 1, done: 2, idle: 3 };
|
.filter(([name]) => name !== 'runtime' && name !== 'frame_engine');
|
||||||
const sorted = Object.entries(_nodeState)
|
|
||||||
.filter(([name]) => name !== 'runtime' && name !== 'frame_engine')
|
const sorted = entries.sort((a, b) => {
|
||||||
.sort((a, b) => (statusOrder[a[1].status] || 3) - (statusOrder[b[1].status] || 3));
|
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 = '';
|
let html = '';
|
||||||
for (const [name, n] of sorted) {
|
for (const [name, n] of sorted) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user