From fde46c28fd6db381201760ea7580d0b6144fc782 Mon Sep 17 00:00:00 2001 From: amosblomqvist Date: Fri, 11 Sep 2026 03:29:21 +0300 Subject: [PATCH] =?UTF-8?q?Activity=20abort=20route:=20POST=20/api/activit?= =?UTF-8?q?y/abort=20=E2=80=94=20uniform=20END=20control=20(jobs=20+=20ext?= =?UTF-8?q?raction)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/activity/abort/route.ts | 42 +++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 app/api/activity/abort/route.ts diff --git a/app/api/activity/abort/route.ts b/app/api/activity/abort/route.ts new file mode 100644 index 0000000..5b5de06 --- /dev/null +++ b/app/api/activity/abort/route.ts @@ -0,0 +1,42 @@ +/** + * POST /api/activity/abort — the dock's END/ABORT control, uniform across + * process kinds (user-decided contract): body {kind: "job"|"extraction", id} + * where id is the jobId for sub-agent jobs and the chatId for extraction + * runs (the pipeline registry is chat-keyed). Mechanisms live in their + * domains — abortJob (server/jobs.ts, agents stream) and abortExtraction + * (the pipeline module, graph stream); this route is the thin composition. + * + * Outcomes: a job's not_found -> 404, not_running -> 409, aborted -> 200. + * Extraction abort is idempotent by design (aborting with nothing in + * flight is success — the blob is gone either way) -> 200. + */ +import { abortExtraction, abortJob } from "@/src/server"; + +export async function POST(req: Request) { + let body: unknown; + try { + body = await req.json(); + } catch { + return Response.json({ error: "Invalid JSON body" }, { status: 400 }); + } + const { kind, id } = (body ?? {}) as { kind?: string; id?: string }; + if ((kind !== "job" && kind !== "extraction") || typeof id !== "string" || id === "") { + return Response.json({ error: 'Body must be {kind: "job"|"extraction", id: string}' }, { status: 400 }); + } + if (!/^[A-Za-z0-9_-]+$/.test(id)) { + return Response.json({ error: "Invalid id" }, { status: 400 }); + } + + if (kind === "job") { + const outcome = abortJob(id); + if (!outcome.ok) { + const status = outcome.reason === "not_found" ? 404 : 409; + return Response.json({ ok: false, reason: outcome.reason }, { status }); + } + return Response.json({ ok: true }); + } + // kind === "extraction": true cancellation; idempotent (nothing in + // flight is success — same dock outcome) + abortExtraction(id); + return Response.json({ ok: true }); +}