feat: post-review backlog hardening (cancel/parser/stall/history/9502)

Five independent items from the post-review backlog. F1: Stop on an external
agent task now aborts the running child via a per-task AbortController registry
reachable from the cancel route, and finalizes the assistant message as
cancelled (fixing two latent bugs — catch blocks left the message streaming,
and warm success-paths wrote complete on an aborted turn); warm pools/worktrees
are preserved and the native path is unchanged. F2/F3: prune the tool-call
parser to its two load-bearing exports (unexport eight zero-caller symbols, add
a gate test for the <invoke>-as-text fallback) and route placeholder-rejection
logging through pino. F6: a 90s per-chunk stall-timeout wraps native inference's
fullStream via AbortSignal.any so a hung stream finalizes the message instead of
hanging — no retry (a pure classifyStreamError helper is added). F7: a read-only
view_session_history MCP tool (newest-N, chronological). F9: retire the unused
apps/coder/web :9502 fallback SPA, keeping every API/WS/health/MCP route.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-03 02:23:11 +00:00
parent 9a139633b8
commit f32fd928b3
48 changed files with 1014 additions and 2254 deletions

View File

@@ -29,6 +29,17 @@ interface ProjectPathRow {
path: string;
}
interface MessageRow {
id: string;
session_id: string;
chat_id: string | null;
role: string;
content: string;
status: string;
model: string | null;
created_at: Date;
}
function textResult(data: unknown) {
return { content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }] };
}
@@ -189,6 +200,56 @@ export async function startMcpServer(sql: Sql): Promise<void> {
},
);
// 6. boocoder.view_session_history
server.tool(
'boocoder.view_session_history',
'Retrieve the most-recent N messages of a session chat transcript (role != system) from messages_with_parts, returned in chronological (oldest→newest) order',
{
session_id: z.string().describe('Session UUID'),
chat_id: z.string().optional().describe('Optional chat UUID — narrows to one chat tab'),
limit: z
.number()
.int()
.min(1)
.max(200)
.optional()
.describe('Max messages to return (default 50, max 200)'),
},
async (args) => {
const effectiveLimit = Math.min(args.limit ?? 50, 200);
let rows: MessageRow[];
if (args.chat_id) {
rows = await sql<MessageRow[]>`
SELECT id, session_id, chat_id, role, content, status, model, created_at
FROM (
SELECT id, session_id, chat_id, role, content, status, model, created_at
FROM messages_with_parts
WHERE session_id = ${args.session_id}
AND chat_id = ${args.chat_id}
AND role != 'system'
ORDER BY created_at DESC
LIMIT ${effectiveLimit}
) sub
ORDER BY created_at ASC
`;
} else {
rows = await sql<MessageRow[]>`
SELECT id, session_id, chat_id, role, content, status, model, created_at
FROM (
SELECT id, session_id, chat_id, role, content, status, model, created_at
FROM messages_with_parts
WHERE session_id = ${args.session_id}
AND role != 'system'
ORDER BY created_at DESC
LIMIT ${effectiveLimit}
) sub
ORDER BY created_at ASC
`;
}
return textResult({ session_id: args.session_id, count: rows.length, messages: rows });
},
);
// Connect via stdio
const transport = new StdioServerTransport();
await server.connect(transport);