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:
Diego Rodrigues de Sa e Souza
2026-09-02 20:37:02 -03:00
committed by GitHub
parent 86e83d4138
commit a628d28898
56 changed files with 2078 additions and 62 deletions

View 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 });
}