mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-15 19:32:20 +03:00
feat(dashboard): orchestration canvas fase 2 — repeat action + A2A memory hits (2.6/2.7) (#12508)
* feat(api): conductor task creation route (repeat support)
* feat(a2a): record memoryHits consulted per task (observability, 2.7)
* feat(dashboard): repeat action in orchestration drawer (2.6 — repeat only)
* fix(dashboard): require every field each repeat contract needs before enabling the action
* feat(dashboard): memory-used drawer section + fase2 i18n/changelog (2.7)
* fix(dashboard): validate memoryHits shape before rendering + locale wording fixes
* fix(dashboard,a2a): stop memoryHits leaking into repeats, harden drawer guard + status clamp
Final whole-branch review fix wave for the Orchestration Canvas Fase 2 PR-C.
- a2a: `createTask` stores a COPY of `input.metadata` instead of aliasing it, so the
observability `memoryHits` written by `executeA2ATaskWithState` no longer leak into
`task.input.metadata`, into the persisted `a2a_tasks.input_json`, or into the drawer's
"Repeat" body (a repeated task was born carrying the previous run's memory snippets,
even with the `OMNIROUTE_A2A_MEMORY_HITS=0` kill-switch on).
- dashboard: `repeatReqFor` strips `memoryHits` from the a2a repeat metadata, so tasks
persisted before the copy-fix do not propagate them either.
- dashboard: the "Memory used" section now requires all four rendered fields (id, key,
type, snippet) to be strings — `{ id: "x", key: { a: 1 } }` used to throw "Objects are
not valid as a React child" and take the whole drawer down.
- dashboard: an `/a2a` action answered with a JSON-RPC error under HTTP 200 is reported as
a failure (`RPC <code>`, code only — never the upstream message) instead of a success
toast; the secured-deployment rejection keeps surfacing the sanitized `HTTP 400`.
- api: the conductor task-creation route clamps a hub status outside 400-599 to 502, so an
out-of-range status can no longer turn a hub refusal into a `RangeError`.
- dashboard: the History tab's `onActionDone` keeps the drawer mounted (and refreshes the
range) instead of closing it, so the repeat/cancel confirmation is actually visible.
- a2a: documented the recall owner-id limitation — `task.owner` is a SHA-256 key prefix
while memory rows are keyed by the DB api-key id, and no hash-to-id lookup exists today,
so recall only resolves under the keyless posture.
* refactor(dashboard): split drawer repeat helpers and test file under the size/complexity gates
---------
Co-authored-by: Markus Hartung <diegosouzapw@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
86e83d4138
commit
a628d28898
66
src/app/api/conductor/tasks/route.ts
Normal file
66
src/app/api/conductor/tasks/route.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* POST /api/conductor/tasks — creates a task on the Conductor hub (Orchestration Canvas
|
||||
* Fase 2, "Repeat" action on the drawer). Thin creation route: validate → auth → delegate
|
||||
* to `createConductorTask`. A hub refusal comes back as the hub's status with a sanitized
|
||||
* body (never the raw upstream body — Hard Rule #12).
|
||||
*/
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createErrorResponse } from "@/lib/api/errorResponse";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { createConductorTask } from "@/lib/conductor/hubProxy";
|
||||
|
||||
/**
|
||||
* Clamps a hub status before it is used as OUR response status. `createConductorTask`
|
||||
* mirrors whatever the hub answered, and `Response.json()` throws a `RangeError` for any
|
||||
* status outside 200-599 — a hub (or a stubbed fetch) answering `0`/`600` would turn a
|
||||
* hub refusal into a 500 from an unhandled throw. A 3xx/2xx reaching this branch is
|
||||
* equally meaningless as an error status, so anything outside 400-599 becomes 502
|
||||
* (Bad Gateway — the honest description of "the upstream hub answered something we
|
||||
* cannot forward").
|
||||
*/
|
||||
function clampErrorStatus(status: unknown): number {
|
||||
const s = Number(status);
|
||||
return Number.isInteger(s) && s >= 400 && s <= 599 ? s : 502;
|
||||
}
|
||||
|
||||
const createTaskSchema = z.object({
|
||||
repoUrl: z.string().min(1),
|
||||
prompt: z.string().min(1),
|
||||
baseRef: z.string().optional(),
|
||||
mode: z.string().optional(),
|
||||
cli: z.string().optional(),
|
||||
model: z.string().optional(),
|
||||
});
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const authError = await requireManagementAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
let rawBody: unknown;
|
||||
try {
|
||||
rawBody = await request.json();
|
||||
} catch {
|
||||
return createErrorResponse({ status: 400, message: "Invalid JSON body" });
|
||||
}
|
||||
|
||||
const parsed = createTaskSchema.safeParse(rawBody);
|
||||
if (!parsed.success) {
|
||||
return createErrorResponse({
|
||||
status: 400,
|
||||
message: "Invalid request body",
|
||||
details: parsed.error.flatten(),
|
||||
});
|
||||
}
|
||||
|
||||
const result = await createConductorTask(parsed.data);
|
||||
if (!result.ok || !result.task_id) {
|
||||
return createErrorResponse({
|
||||
status: clampErrorStatus(result.status),
|
||||
message: `Conductor hub refused the task creation (HTTP ${result.status})`,
|
||||
});
|
||||
}
|
||||
return NextResponse.json({ task_id: result.task_id }, { status: 201 });
|
||||
}
|
||||
Reference in New Issue
Block a user