feat(mma): complete Phase 6 and finalize Comprehensive GUI UX track

- Implement Live Worker Streaming: wire ai_client.comms_log_callback to Tier 3 streams
- Add Parallel DAG Execution using asyncio.gather for non-dependent tickets
- Implement Automatic Retry with Model Escalation (Flash-Lite -> Flash -> Pro)
- Add Tier Model Configuration UI to MMA Dashboard with project TOML persistence
- Fix FPS reporting in PerformanceMonitor to prevent transient 0.0 values
- Update Ticket model with retry_count and dictionary-like access
- Stabilize Gemini CLI integration tests and handle script approval events in simulations
- Finalize and verify all 6 phases of the implementation plan
This commit is contained in:
2026-03-01 22:38:43 -05:00
parent d1ce0eaaeb
commit 9fb01ce5d1
22 changed files with 756 additions and 498 deletions

View File

@@ -62,7 +62,6 @@ class HookHandler(BaseHTTPRequestHandler):
body = self.rfile.read(content_length)
data = json.loads(body.decode('utf-8'))
field_tag = data.get("field")
print(f"[DEBUG] Hook Server: get_value for {field_tag}")
event = threading.Event()
result = {"value": None}
@@ -71,10 +70,7 @@ class HookHandler(BaseHTTPRequestHandler):
if field_tag in app._settable_fields:
attr = app._settable_fields[field_tag]
val = getattr(app, attr, None)
print(f"[DEBUG] Hook Server: attr={attr}, val={val}")
result["value"] = val
else:
print(f"[DEBUG] Hook Server: {field_tag} NOT in settable_fields")
finally:
event.set()
with app._pending_gui_tasks_lock:
@@ -133,10 +129,8 @@ class HookHandler(BaseHTTPRequestHandler):
result["pending_script_approval"] = getattr(app, "_pending_dialog", None) is not None
result["pending_mma_step_approval"] = getattr(app, "_pending_mma_approval", None) is not None
result["pending_mma_spawn_approval"] = getattr(app, "_pending_mma_spawn", None) is not None
# Keep old fields for backward compatibility but add specific ones above
result["pending_approval"] = result["pending_mma_step_approval"] or result["pending_tool_approval"]
result["pending_spawn"] = result["pending_mma_spawn_approval"]
# Added lines for tracks and proposed_tracks
result["tracks"] = getattr(app, "tracks", [])
result["proposed_tracks"] = getattr(app, "proposed_tracks", [])
result["mma_streams"] = getattr(app, "mma_streams", {})
@@ -157,13 +151,11 @@ class HookHandler(BaseHTTPRequestHandler):
self.send_response(504)
self.end_headers()
elif self.path == '/api/gui/diagnostics':
# Safe way to query multiple states at once via the main thread queue
event = threading.Event()
result = {}
def check_all():
try:
# Generic state check based on App attributes (works for both DPG and ImGui versions)
status = getattr(app, "ai_status", "idle")
result["thinking"] = status in ["sending...", "running powershell..."]
result["live"] = status in ["running powershell...", "fetching url...", "searching web...", "powershell done, awaiting AI..."]
@@ -201,57 +193,55 @@ class HookHandler(BaseHTTPRequestHandler):
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(
json.dumps({'status': 'updated'}).encode('utf-8'))
self.wfile.write(json.dumps({'status': 'updated'}).encode('utf-8'))
elif self.path.startswith('/api/confirm/'):
action_id = self.path.split('/')[-1]
approved = data.get('approved', False)
if hasattr(app, 'resolve_pending_action'):
success = app.resolve_pending_action(action_id, approved)
if success:
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps({'status': 'ok'}).encode('utf-8'))
else:
self.send_response(404)
self.end_headers()
else:
self.send_response(500)
self.end_headers()
elif self.path == '/api/session':
app.disc_entries = data.get('session', {}).get(
'entries', app.disc_entries)
app.disc_entries = data.get('session', {}).get('entries', app.disc_entries)
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(
json.dumps({'status': 'updated'}).encode('utf-8'))
self.wfile.write(json.dumps({'status': 'updated'}).encode('utf-8'))
elif self.path == '/api/gui':
with app._pending_gui_tasks_lock:
app._pending_gui_tasks.append(data)
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(
json.dumps({'status': 'queued'}).encode('utf-8'))
self.wfile.write(json.dumps({'status': 'queued'}).encode('utf-8'))
elif self.path == '/api/ask':
request_id = str(uuid.uuid4())
event = threading.Event()
if not hasattr(app, '_pending_asks'):
app._pending_asks = {}
if not hasattr(app, '_ask_responses'):
app._ask_responses = {}
if not hasattr(app, '_pending_asks'): app._pending_asks = {}
if not hasattr(app, '_ask_responses'): app._ask_responses = {}
app._pending_asks[request_id] = event
# Emit event for test/client discovery
with app._api_event_queue_lock:
app._api_event_queue.append({
"type": "ask_received",
"request_id": request_id,
"data": data
})
app._api_event_queue.append({"type": "ask_received", "request_id": request_id, "data": data})
with app._pending_gui_tasks_lock:
app._pending_gui_tasks.append({
"type": "ask",
"request_id": request_id,
"data": data
})
app._pending_gui_tasks.append({"type": "ask", "request_id": request_id, "data": data})
if event.wait(timeout=60.0):
response_data = app._ask_responses.get(request_id)
# Clean up response after reading
if request_id in app._ask_responses:
del app._ask_responses[request_id]
if request_id in app._ask_responses: del app._ask_responses[request_id]
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps({'status': 'ok', 'response': response_data}).encode('utf-8'))
else:
if request_id in app._pending_asks:
del app._pending_asks[request_id]
if request_id in app._pending_asks: del app._pending_asks[request_id]
self.send_response(504)
self.end_headers()
self.wfile.write(json.dumps({'error': 'timeout'}).encode('utf-8'))
@@ -262,14 +252,9 @@ class HookHandler(BaseHTTPRequestHandler):
app._ask_responses[request_id] = response_data
event = app._pending_asks[request_id]
event.set()
# Clean up pending ask entry
del app._pending_asks[request_id]
# Queue GUI task to clear the dialog
with app._pending_gui_tasks_lock:
app._pending_gui_tasks.append({
"action": "clear_ask",
"request_id": request_id
})
app._pending_gui_tasks.append({"action": "clear_ask", "request_id": request_id})
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
@@ -302,21 +287,12 @@ class HookServer:
is_gemini_cli = getattr(self.app, 'current_provider', '') == 'gemini_cli'
if not getattr(self.app, 'test_hooks_enabled', False) and not is_gemini_cli:
return
# Ensure the app has the task queue and lock initialized
if not hasattr(self.app, '_pending_gui_tasks'):
self.app._pending_gui_tasks = []
if not hasattr(self.app, '_pending_gui_tasks_lock'):
self.app._pending_gui_tasks_lock = threading.Lock()
# Initialize ask-related dictionaries
if not hasattr(self.app, '_pending_asks'):
self.app._pending_asks = {}
if not hasattr(self.app, '_ask_responses'):
self.app._ask_responses = {}
# Event queue for test script subscriptions
if not hasattr(self.app, '_api_event_queue'):
self.app._api_event_queue = []
if not hasattr(self.app, '_api_event_queue_lock'):
self.app._api_event_queue_lock = threading.Lock()
if not hasattr(self.app, '_pending_gui_tasks'): self.app._pending_gui_tasks = []
if not hasattr(self.app, '_pending_gui_tasks_lock'): self.app._pending_gui_tasks_lock = threading.Lock()
if not hasattr(self.app, '_pending_asks'): self.app._pending_asks = {}
if not hasattr(self.app, '_ask_responses'): self.app._ask_responses = {}
if not hasattr(self.app, '_api_event_queue'): self.app._api_event_queue = []
if not hasattr(self.app, '_api_event_queue_lock'): self.app._api_event_queue_lock = threading.Lock()
self.server = HookServerInstance(('127.0.0.1', self.port), HookHandler, self.app)
self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
self.thread.start()
@@ -329,5 +305,3 @@ class HookServer:
if self.thread:
self.thread.join()
logging.info("Hook server stopped")