mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-14 02:42:24 +03:00
* feat(api): hydrate memoryHits from the persisted history event
`GET /api/a2a/tasks/[id]` falls back to the persisted history row once a task
leaves the in-memory TTL window, and `reconstituteHistoricalTask` hard-coded
`metadata: {}` — so the drawer's "Memory used" section vanished for any
historical task, even though `executeA2ATaskWithState` had already written a
`memory_hits` event with the hits.
The fallback now reads that event: `data_json` is parsed and, when it yields at
least one well-formed hit, exposed as `metadata.memoryHits`. The event itself is
filtered out of `events` — it is observability, not a state transition, and
without the filter it leaked into the timeline as a duplicate of the row's
current state.
Reading is defensive throughout, mirroring `DrawerMemory`'s own validation: the
payload is caller-influenced and unvalidated end to end, so `JSON.parse` runs
inside `safeJsonParse`, non-arrays are rejected, and each entry must carry `id`,
`key`, `type` and `snippet` as strings (a non-string field would be rendered as
a React child and take the drawer down). Malformed input degrades to
`metadata: {}` and a 200 — never a 500.
Refs #12639
* fix(a2a): bound the memory recall with its own deadline
collectMemoryHits() runs BEFORE the skill handler and had no deadline at all,
so a slow memory backend delayed the start of every A2A task — the HTTP
genericBackend alone defaults to a 30s timeout.
The search now races a MEMORY_RECALL_TIMEOUT_MS (1500ms) deadline. Overshooting
degrades exactly like any other recall failure: empty hits, a warn log, and the
task proceeds normally (best-effort contract unchanged, nothing propagates).
The deadline timer is cleared in a finally on BOTH paths so no handle is left
holding the event loop open, and MemoryHitsDeps.timeoutMs makes it injectable
so the tests cost milliseconds instead of 1.5s of wall clock.
Refs #12639
* fix(dashboard): carry conductor requirements and focus the repeated task
The drawer's "Repeat" for a Conductor task dropped the runner/model pinning and
left the operator staring at the finished run:
- `hubTaskSchema` now parses the hub's `requirements` (`.catch(null)` so an odd
shape never fails the whole task parse), and `ConductorTaskDetail` exposes
`cli`/`model` (`null` when the hub sends none).
- `repeatReqForConductor` carries `cli`/`model` when present and OMITS them
otherwise — the route's Zod takes both as optional strings, so a `null` would
400. The two fields are independent.
- `performAction` reads the response body once and returns it, so the repeat can
report the CANVAS id of the created task (`task_id` / `data.id` /
`result.task.id`, each with its node prefix). `OrchestrationPageClient` then
refetches and focuses it via `?node=`; History keeps its current behavior.
- `conductor-routes-auth.test.ts` covers the creation route through its `ROUTES`
array; the duplicated source assertion left `conductor-create-route.test.ts`.
Refs #12639
* chore(a2a): follow-ups changelog
Changelog fragment for the five items PR-C delivers from #12639.
The sixth item on the issue — an authenticated panel path for A2A task
creation — stays deliberately out of scope and is recorded as such in a
comment on the issue rather than silently dropped: the JSON-RPC endpoint
accepts API keys only, and widening that endpoint's auth surface to serve a
UI convenience is the operator's call, not the implementation's.
Closes #12639
160 lines
8.6 KiB
Markdown
160 lines
8.6 KiB
Markdown
# Task C3 — `requirements` no repeat do Conductor + foco na task nova + teste de auth
|
||
|
||
Branch `feat/orch-fase3-c` · commit **`a7d430fe9ee3ae1fd1d86ff0083318fc1e96005b`**
|
||
(`fix(dashboard): carry conductor requirements and focus the repeated task`, corpo com `Refs #12639`).
|
||
|
||
## O que mudou, por item
|
||
|
||
### 1. `src/lib/conductor/hubProxy.ts` — `requirements` no schema de detalhe
|
||
|
||
- `hubTaskSchema` ganhou
|
||
`requirements: z.object({ cli: z.string().nullish(), model: z.string().nullish() }).nullish().catch(null)`.
|
||
O `.catch(null)` é deliberado: o MESMO schema parseia a **lista** em `getFleetSnapshot`
|
||
(`z.array(hubTaskSchema).parse(rawTasks)`) — sem ele, um `requirements` de formato inesperado
|
||
numa única task faria o `parse` inteiro lançar e o snapshot cair para `{offline: true}`.
|
||
- `ConductorTaskDetail` ganhou `cli: string | null` e `model: string | null` (com JSDoc);
|
||
`getConductorTaskDetail` preenche com `t.requirements?.cli ?? null` / `?? null`.
|
||
- Origem do shape: é exatamente o objeto que `createConductorTask` (mesmo arquivo, linhas ~238-241)
|
||
envia para o hub (`body.requirements = { cli?, model? }`) — não foi inventado.
|
||
|
||
### 2. `drawer/useDrawerDetail.ts`
|
||
|
||
- `repeatReqForConductor` agora envia `cli: d.cli ?? undefined` / `model: d.model ?? undefined`.
|
||
**`undefined`, nunca `null`** — o Zod real da rota (`src/app/api/conductor/tasks/route.ts`) tem
|
||
`cli: z.string().optional()` / `model: z.string().optional()`, e `null` daria 400. `JSON.stringify`
|
||
descarta as chaves `undefined`, então o corpo sai idêntico ao de hoje quando não há requirement.
|
||
Os dois campos são independentes (um pode existir sem o outro) — há teste para esse caso.
|
||
- `jsonRpcErrorCode` deixou de ler `res.json()` e passou a receber o **corpo já parseado**; novo
|
||
`readJsonBody(res)` lê o corpo UMA vez (um body só pode ser consumido uma vez, e agora o id da
|
||
task nova sai do mesmo corpo). Body não-JSON/vazio ⇒ `null`, sem virar falha (mantém o contrato
|
||
anterior: "corpo ilegível não é evidência de falha, o status já valeu").
|
||
- `performAction` passou a devolver `{ ok, body }` em vez de `boolean`.
|
||
- Novo `export function newNodeIdFrom(node, body): string | null` — extrai o id da task criada e
|
||
devolve **já no formato do canvas** (com prefixo), porque `setParams({ node })` casa contra
|
||
`snapshot.nodes.find(n => n.id === nodeId)` e os ids em `mergeSnapshot.ts` são prefixados:
|
||
- conductor: `body.task_id` → `conductor:task:<id>`
|
||
- cloud-agent: `body.data.id` → `cloud-agent:<id>`
|
||
- a2a: `body.result.task.id` → `a2a:<id>` (envelope real de `/a2a` `message/send` v0.3)
|
||
Leitura defensiva: id ausente/vazio/não-string ⇒ `null` (nunca um prefixo pelado), corpo não-objeto
|
||
⇒ `null`, nunca lança.
|
||
- No hook: `approve`/`cancel` continuam `Promise<boolean>` (via `runBooleanAction`) — de propósito:
|
||
eles agem sobre a task já aberta e a resposta do cloud-agent pode ecoar o id da MESMA task, o que
|
||
faria o painel "navegar" para onde já está. Só `repeat` devolve
|
||
`RepeatOutcome { ok, newNodeId }` (interface exportada).
|
||
|
||
### 3. `drawer/OrchestrationDrawer.tsx`
|
||
|
||
- `RepeatButton` consome `RepeatOutcome` e chama `onActionDone(newNodeId ?? undefined)`.
|
||
- `onActionDone` virou `(newNodeId?: string) => void` no `RepeatButton`, `DrawerActions` e no
|
||
componente exportado. `DrawerActions.run()` (approve/cancel) segue chamando `onActionDone()` sem
|
||
argumento.
|
||
|
||
### 4. `OrchestrationPageClient.tsx`
|
||
|
||
- Novo `onActionDone` memoizado: `refetch()` e, se veio id, `setParams({ node: newNodeId })`.
|
||
A aba History renderiza o próprio drawer (`HistoryTab.tsx`) e **não** foi tocada — seu
|
||
`onActionDone={() => …}` ignora o argumento extra, então o comportamento lá continua idêntico
|
||
(re-amostra `nowMs`, não navega), como o plano pede.
|
||
|
||
### 5. Testes
|
||
|
||
- `tests/unit/conductor-routes-auth.test.ts`: `src/app/api/conductor/tasks/route.ts` entrou no array
|
||
`ROUTES`; o regex do proxy ganhou a alternativa `createConductorTask\(`.
|
||
- `tests/unit/conductor-create-route.test.ts`: removido o teste de fonte duplicado
|
||
(`route: requireManagementAuth antes de criar a task no hub`), substituído por um comentário
|
||
apontando para o array. `fs`/`path` continuam usados pelo resto do arquivo.
|
||
- `tests/unit/ui/orchestrationDrawerRepeat.test.tsx` (786 → 971 linhas, cap 1200 — não precisou
|
||
dividir): 3 describes novos, 6 testes:
|
||
- `repeatReqFor` conductor com `cli`/`model`; com ambos `null` (corpo idêntico ao atual e sem as
|
||
strings `cli`/`model`); com só `cli` setado.
|
||
- `newNodeIdFrom` nas três fontes (prefixado), corpos malformados ⇒ `null`, fonte sem contrato ⇒ `null`.
|
||
- drawer: repeat de conductor faz POST com `cli`/`model` e chama `onActionDone` exatamente uma vez
|
||
com `"conductor:task:t_new"`, com o toast `repeatDone`.
|
||
|
||
## TDD — evidência
|
||
|
||
**C3.2 (vermelho)** `npx vitest run tests/unit/ui/orchestrationDrawerRepeat.test.tsx`:
|
||
|
||
```
|
||
Test Files 1 failed (1)
|
||
Tests 6 failed | 29 passed (35)
|
||
```
|
||
|
||
(falhas: `newNodeIdFrom is not a function` ×3, corpo sem `cli`/`model` ×3 — a última:
|
||
`expected { …(4) } to match object { cli: 'claude', model: 'sonnet' }`)
|
||
|
||
`node --import tsx/esm --test tests/unit/conductor-routes-auth.test.ts tests/unit/conductor-create-route.test.ts` já passou nesse ponto
|
||
(`ℹ pass 11 / fail 0`) — **divergência menor com o plano**: a mudança do array `ROUTES` é ampliação
|
||
de cobertura + de-duplicação, não a prova de um defeito, então ela não tinha como "falhar antes".
|
||
A rota já chamava `requireManagementAuth` corretamente.
|
||
|
||
**C3.4 (verde)**:
|
||
|
||
```
|
||
$ npx vitest run tests/unit/ui/orchestrationDrawerRepeat.test.tsx
|
||
Test Files 1 passed (1)
|
||
Tests 35 passed (35)
|
||
|
||
$ node --import tsx/esm --test tests/unit/conductor-routes-auth.test.ts tests/unit/conductor-create-route.test.ts tests/unit/client-bundle-no-server-only-10692.test.ts
|
||
✔ src/app/api/conductor/tasks/route.ts: requireManagementAuth antes do proxy ao hub (4.175999ms)
|
||
ℹ tests 12
|
||
ℹ pass 12
|
||
ℹ fail 0
|
||
|
||
$ npx vitest run tests/unit/ui/
|
||
Test Files 210 passed (210)
|
||
Tests 1200 passed (1200)
|
||
Duration 179.85s
|
||
|
||
$ node --import tsx/esm --test tests/unit/conductor-hub-proxy.test.ts tests/unit/conductor-fleet-mirror.test.ts tests/unit/conductor-fleet-route.test.ts tests/unit/conductor-delegate.test.ts
|
||
ℹ tests 17
|
||
ℹ pass 17
|
||
ℹ fail 0
|
||
```
|
||
|
||
## Gates
|
||
|
||
```
|
||
$ npm run typecheck:core
|
||
EXIT=0 (sem saída de erro)
|
||
|
||
$ npm run check:dashboard-typecheck
|
||
[dashboard-typecheck] OK — 206 pre-existing error(s), all within frozen baseline.
|
||
EXIT=0
|
||
|
||
$ npx eslint <os 7 arquivos alterados>
|
||
EXIT=0 (zero erros, zero warnings)
|
||
|
||
$ npm run check:file-size
|
||
[file-size] OK — 136 arquivos congelados, cap 1200 para novos (4588 arquivos verificados)
|
||
[test-file-size] OK — 40 test files congelados, testCap 1200 para novos (5495 test files verificados)
|
||
```
|
||
|
||
Hooks do husky rodaram no commit (lint-staged + docs-sync + any-budget + tracked-artifacts, todos PASS);
|
||
nada de `--no-verify`.
|
||
|
||
## Divergências entre plano e código real
|
||
|
||
1. **`newNodeIdFrom` prefixado (não previsto explicitamente no plano).** O plano diz "extrair o id da
|
||
task nova (`task_id` no Conductor, `data.id` no Cloud Agent) e chamar `onActionDone` com ele". O id
|
||
CRU não serve para `setParams({ node })`: o canvas indexa por id prefixado
|
||
(`conductor:task:<id>`, `cloud-agent:<id>`, `a2a:<id>` — `mergeSnapshot.ts` / `routeFor`). Segui o
|
||
código: `newNodeIdFrom` já devolve o id de canvas. O plano não citava o a2a; incluí porque o
|
||
`repeat` do drawer também atende a2a e o envelope (`result.task.id`) é verificável em
|
||
`src/app/a2a/route.ts:250`.
|
||
2. **`performAction` "devolve o corpo parseado" — só o `repeat` propaga o id.** `approve`/`cancel`
|
||
ficaram `Promise<boolean>` de propósito (ver item 2 acima): o POST de approve/cancel do cloud-agent
|
||
pode devolver o id da MESMA task, e navegar para ela seria ruído. Cumprimento do espírito do plano
|
||
("o drawer extrai o id da task nova") sem o efeito colateral.
|
||
3. **`.catch(null)` no `requirements`.** O plano não pedia; adicionei porque `hubTaskSchema` também
|
||
parseia a LISTA de tasks, e um campo novo estrito ali seria um vetor de "snapshot vira offline por
|
||
causa de um campo estranho de uma task".
|
||
4. **A mudança em `conductor-routes-auth.test.ts` não é um teste que falha antes** (explicado acima).
|
||
5. `ConductorTaskDetail` estende `FleetTask`; `cli`/`model` foram adicionados só no detalhe (não em
|
||
`FleetTask`/`toFleetTask`), porque o repeat lê o DETALHE carregado, não o nó.
|
||
|
||
## Fora de escopo (não feito, é da Task C4)
|
||
|
||
`changelog.d/fixes/orchestration-followups.md`, o comentário na issue #12639 e a bateria completa
|
||
(lint total, test:vitest, check:cycles, i18n) continuam pendentes na C4.
|