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

@@ -8,6 +8,12 @@ interface InferenceApi {
cancel: (sessionId: string, chatId: string) => Promise<boolean>;
}
// F1: the dispatcher's reach into an in-flight external-agent run. Narrow by
// design (not the whole dispatcher) — the route only needs to fire the abort.
// Returns true when a controller was registered for the task (an external run was
// in flight), false otherwise (native boocode task, or already finished).
export type ExternalCancelFn = (taskId: string) => boolean;
const CreateBody = z.object({
project_id: z.string().uuid(),
input: z.string().min(1).max(64_000),
@@ -27,7 +33,12 @@ const ListQuery = z.object({
project_id: z.string().uuid().optional(),
});
export function registerTaskRoutes(app: FastifyInstance, sql: Sql, inference: InferenceApi): void {
export function registerTaskRoutes(
app: FastifyInstance,
sql: Sql,
inference: InferenceApi,
cancelExternal: ExternalCancelFn,
): void {
// POST /api/tasks — create a new task
app.post('/api/tasks', async (req, reply) => {
const parsed = CreateBody.safeParse(req.body);
@@ -127,7 +138,14 @@ export function registerTaskRoutes(app: FastifyInstance, sql: Sql, inference: In
cancelPendingPermission(taskId);
// If running, try to cancel inference
// F1: abort the in-flight external-agent run (opencode / goose / qwen / claude).
// Idempotent — a double-Stop re-aborts harmlessly; a native boocode task is not
// registered, so this returns false and the inference.cancel path below handles
// it unchanged. The dispatcher's run-function finalizes the streaming assistant
// message as 'cancelled' once the backend honors the signal.
cancelExternal(taskId);
// If running, try to cancel inference (native boocode path — unchanged).
if ((task.state === 'running' || task.state === 'blocked') && task.session_id) {
// Find active chat in the task's session
const chats = await sql<{ id: string }[]>`