fix(path-b): dev_server tool dispatch + state-machine transition
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
This commit is contained in:
@@ -369,13 +369,21 @@ export async function POST(request: Request) {
|
||||
case 'fs.grep':
|
||||
return await toolFsGrep(principal, params);
|
||||
|
||||
// The Gemini tool-name "dev_server_list" maps to dotted action
|
||||
// "dev.server.list" via executeMcpTool's underscore→dot replace.
|
||||
// We accept BOTH the original underscore form and the converted
|
||||
// dotted form so external MCP clients aren't surprised either way.
|
||||
case 'dev_server.start':
|
||||
case 'dev.server.start':
|
||||
return await toolDevServerStart(principal, params);
|
||||
case 'dev_server.stop':
|
||||
case 'dev.server.stop':
|
||||
return await toolDevServerStop(principal, params);
|
||||
case 'dev_server.list':
|
||||
case 'dev.server.list':
|
||||
return await toolDevServerList(principal, params);
|
||||
case 'dev_server.logs':
|
||||
case 'dev.server.logs':
|
||||
return await toolDevServerLogs(principal, params);
|
||||
case 'ship':
|
||||
return await toolShip(principal, params);
|
||||
|
||||
@@ -275,8 +275,16 @@ export async function resumeDevContainer(projectId: string): Promise<void> {
|
||||
}
|
||||
|
||||
async function touchActivity(projectId: string): Promise<void> {
|
||||
// Also flips state 'provisioning' → 'running' on first successful exec.
|
||||
// We can't rely on Coolify's deploy webhook alone (it fires before the
|
||||
// container's actually accepting docker exec), so the first exec that
|
||||
// returns is our authoritative liveness signal.
|
||||
await query(
|
||||
`UPDATE fs_project_dev_containers SET last_active_at = now() WHERE project_id = $1`,
|
||||
`UPDATE fs_project_dev_containers
|
||||
SET last_active_at = now(),
|
||||
state = CASE WHEN state IN ('provisioning','suspended') THEN 'running' ELSE state END,
|
||||
suspended_at = NULL
|
||||
WHERE project_id = $1`,
|
||||
[projectId],
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user