MAESTRO: fix MCP type coercion for batch endpoints, add defensive observation error handling
Add string-to-array coercion for ids and memorySessionIds in DataRoutes.ts batch endpoints so MCP clients sending "[1,2,3]" or "1,2,3" instead of native arrays no longer get 400 errors. Wrap observation storage path in SessionRoutes.ts with try/catch returning 200 on recoverable errors instead of 500, preventing hook breakage. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -114,7 +114,12 @@ export class DataRoutes extends BaseRouteHandler {
|
||||
* Body: { ids: number[], orderBy?: 'date_desc' | 'date_asc', limit?: number, project?: string }
|
||||
*/
|
||||
private handleGetObservationsByIds = this.wrapHandler((req: Request, res: Response): void => {
|
||||
const { ids, orderBy, limit, project } = req.body;
|
||||
let { ids, orderBy, limit, project } = req.body;
|
||||
|
||||
// Coerce string-encoded arrays from MCP clients (e.g. "[1,2,3]" or "1,2,3")
|
||||
if (typeof ids === 'string') {
|
||||
try { ids = JSON.parse(ids); } catch { ids = ids.split(',').map(Number); }
|
||||
}
|
||||
|
||||
if (!ids || !Array.isArray(ids)) {
|
||||
this.badRequest(res, 'ids must be an array of numbers');
|
||||
@@ -163,7 +168,12 @@ export class DataRoutes extends BaseRouteHandler {
|
||||
* Body: { memorySessionIds: string[] }
|
||||
*/
|
||||
private handleGetSdkSessionsByIds = this.wrapHandler((req: Request, res: Response): void => {
|
||||
const { memorySessionIds } = req.body;
|
||||
let { memorySessionIds } = req.body;
|
||||
|
||||
// Coerce string-encoded arrays from MCP clients (e.g. '["a","b"]' or "a,b")
|
||||
if (typeof memorySessionIds === 'string') {
|
||||
try { memorySessionIds = JSON.parse(memorySessionIds); } catch { memorySessionIds = memorySessionIds.split(',').map((s: string) => s.trim()); }
|
||||
}
|
||||
|
||||
if (!Array.isArray(memorySessionIds)) {
|
||||
this.badRequest(res, 'memorySessionIds must be an array');
|
||||
|
||||
@@ -504,57 +504,63 @@ export class SessionRoutes extends BaseRouteHandler {
|
||||
}
|
||||
}
|
||||
|
||||
const store = this.dbManager.getSessionStore();
|
||||
try {
|
||||
const store = this.dbManager.getSessionStore();
|
||||
|
||||
// Get or create session
|
||||
const sessionDbId = store.createSDKSession(contentSessionId, '', '');
|
||||
const promptNumber = store.getPromptNumberFromUserPrompts(contentSessionId);
|
||||
// Get or create session
|
||||
const sessionDbId = store.createSDKSession(contentSessionId, '', '');
|
||||
const promptNumber = store.getPromptNumberFromUserPrompts(contentSessionId);
|
||||
|
||||
// Privacy check: skip if user prompt was entirely private
|
||||
const userPrompt = PrivacyCheckValidator.checkUserPromptPrivacy(
|
||||
store,
|
||||
contentSessionId,
|
||||
promptNumber,
|
||||
'observation',
|
||||
sessionDbId,
|
||||
{ tool_name }
|
||||
);
|
||||
if (!userPrompt) {
|
||||
res.json({ status: 'skipped', reason: 'private' });
|
||||
return;
|
||||
// Privacy check: skip if user prompt was entirely private
|
||||
const userPrompt = PrivacyCheckValidator.checkUserPromptPrivacy(
|
||||
store,
|
||||
contentSessionId,
|
||||
promptNumber,
|
||||
'observation',
|
||||
sessionDbId,
|
||||
{ tool_name }
|
||||
);
|
||||
if (!userPrompt) {
|
||||
res.json({ status: 'skipped', reason: 'private' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Strip memory tags from tool_input and tool_response
|
||||
const cleanedToolInput = tool_input !== undefined
|
||||
? stripMemoryTagsFromJson(JSON.stringify(tool_input))
|
||||
: '{}';
|
||||
|
||||
const cleanedToolResponse = tool_response !== undefined
|
||||
? stripMemoryTagsFromJson(JSON.stringify(tool_response))
|
||||
: '{}';
|
||||
|
||||
// Queue observation
|
||||
this.sessionManager.queueObservation(sessionDbId, {
|
||||
tool_name,
|
||||
tool_input: cleanedToolInput,
|
||||
tool_response: cleanedToolResponse,
|
||||
prompt_number: promptNumber,
|
||||
cwd: cwd || (() => {
|
||||
logger.error('SESSION', 'Missing cwd when queueing observation in SessionRoutes', {
|
||||
sessionId: sessionDbId,
|
||||
tool_name
|
||||
});
|
||||
return '';
|
||||
})()
|
||||
});
|
||||
|
||||
// Ensure SDK agent is running
|
||||
this.ensureGeneratorRunning(sessionDbId, 'observation');
|
||||
|
||||
// Broadcast observation queued event
|
||||
this.eventBroadcaster.broadcastObservationQueued(sessionDbId);
|
||||
|
||||
res.json({ status: 'queued' });
|
||||
} catch (error) {
|
||||
// Return 200 on recoverable errors so the hook doesn't break
|
||||
logger.error('SESSION', 'Observation storage failed', { contentSessionId, tool_name }, error as Error);
|
||||
res.json({ stored: false, reason: (error as Error).message });
|
||||
}
|
||||
|
||||
// Strip memory tags from tool_input and tool_response
|
||||
const cleanedToolInput = tool_input !== undefined
|
||||
? stripMemoryTagsFromJson(JSON.stringify(tool_input))
|
||||
: '{}';
|
||||
|
||||
const cleanedToolResponse = tool_response !== undefined
|
||||
? stripMemoryTagsFromJson(JSON.stringify(tool_response))
|
||||
: '{}';
|
||||
|
||||
// Queue observation
|
||||
this.sessionManager.queueObservation(sessionDbId, {
|
||||
tool_name,
|
||||
tool_input: cleanedToolInput,
|
||||
tool_response: cleanedToolResponse,
|
||||
prompt_number: promptNumber,
|
||||
cwd: cwd || (() => {
|
||||
logger.error('SESSION', 'Missing cwd when queueing observation in SessionRoutes', {
|
||||
sessionId: sessionDbId,
|
||||
tool_name
|
||||
});
|
||||
return '';
|
||||
})()
|
||||
});
|
||||
|
||||
// Ensure SDK agent is running
|
||||
this.ensureGeneratorRunning(sessionDbId, 'observation');
|
||||
|
||||
// Broadcast observation queued event
|
||||
this.eventBroadcaster.broadcastObservationQueued(sessionDbId);
|
||||
|
||||
res.json({ status: 'queued' });
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user