Two bugs caught by the live end-to-end test:
1. Tool dispatch mismatch.
Gemini tool name "dev_server_list" runs through executeMcpTool's
_-to-. converter (toolName.replace(/_/g, '.')) and arrives as
"dev.server.list". The dispatcher only had cases for "dev_server.list",
so all four dev_server.* tools 404'd as "Unknown tool".
The AI gracefully fell back to shell.exec + nohup, so Express still
ran — but the dev_servers table never got populated and the preview
URL machinery was bypassed. Add aliases for both underscore and
fully-dotted forms.
2. State machine never transitioned.
ensureDevContainer wrote state='provisioning'; nothing ever flipped
it to 'running'. As a result the idle-sweep (which filters by
state='running') never saw a candidate to suspend.
Use the first successful exec as the authoritative liveness signal:
touchActivity() now also flips provisioning|suspended → running and
clears suspended_at.
Surfaced by the live trace: AI tried dev_server_list, got 404, fell
back to manually grepping the process table.
Made-with: Cursor