Files
OmniRoute/task-c1-report.md
Diego Rodrigues de Sa e Souza 590582711c fix(dashboard): orchestration canvas fase 3 — follow-ups da review final (#12639) (#12988)
* 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
2026-09-10 10:23:52 -03:00

3.7 KiB
Raw Blame History

Task C1 — History hidrata memoryHits (PR-C, Refs #12639)

Commit: da8014442debd2eca7a326472ab704aa86aefba1 Branch: feat/orch-fase3-c · 2 arquivos, +164/-16

O que mudou

src/app/api/a2a/tasks/[id]/route.ts

  • Novas constantes MEMORY_HITS_EVENT_TYPE = "memory_hits" e MEMORY_HIT_FIELDS = ["id","key","type","snippet"], interface MemoryHit.
  • Nova função parseMemoryHits(dataJson): safeJsonParse (try/catch já existente) → Array.isArray → filtro por item exigindo os 4 campos como string. Nunca lança; payload ruim vira lista vazia.
  • reconstituteHistoricalTask: listA2ATaskEvents(row.id) agora é lido uma vez em eventRows; os eventos memory_hits são extraídos para memoryHits e filtrados fora de events; metadata passou de {} fixo para memoryHits.length > 0 ? { memoryHits } : {}.

tests/unit/a2a-history-route.test.ts (+104 linhas, 365 no total — teto 1200 ok)

7 testes novos:

  1. hidrata metadata.memoryHits com 2 hits e o evento de memória NÃO aparece em events (timeline fica ["submitted","completed"]).
  2. descarta entradas malformadas (sem snippet, key objeto, null, string, número) e mantém só a válida. 37. tabela de payloads ruins ({not-json, string JSON pura, objeto JSON, array 100% inválido, array vazio) ⇒ HTTP 200, metadata {}, e o evento de memória continua fora da timeline.

Gates (saída real)

$ node --import tsx/esm --test tests/unit/a2a-history-route.test.ts   # ANTES do fix (C1.2)
✔ 10 pré-existentes  ✖ 7 novos   (falha típica: metadata {} esperado ≠ { memoryHits: [...] } / timeline extra 'completed')

$ node --import tsx/esm --test tests/unit/a2a-history-route.test.ts   # DEPOIS (C1.4)
 tests 17
 pass 17
 fail 0

$ node --import tsx/esm --test tests/unit/db-a2a-tasks.test.ts
 tests 7
 pass 7
 fail 0

$ npm run typecheck:core
npm notice run tsc --pretty false -p tsconfig.typecheck-core.json
(sem erros, exit 0)

$ npx eslint 'src/app/api/a2a/tasks/[id]/route.ts' tests/unit/a2a-history-route.test.ts
eslint exit=0

$ git commit  (husky ativo, sem --no-verify)
lint-staged: prettier + eslint --fix  ✔
[docs-sync] PASS
[t11:any-budget] PASS
[tracked-artifacts] OK

Divergências entre plano e código real

Nenhuma divergência material. Confirmado no código:

  • event_type exato = "memory_hits" (src/lib/a2a/taskExecution.ts:143, appendA2ATaskEvent(task.id, "memory_hits", JSON.stringify(hits))).
  • Shape de listA2ATaskEvents = { event_type, data_json, created_at } (src/lib/db/a2aTasks.ts, A2ATaskHistoryEventRow) — sem id exposto, ordenação ORDER BY id ASC.
  • STATE_EVENT_PREFIX = "state:" continua governando só os eventos de transição; o memory_hits caía no else e herdava row.state, o que o fazia aparecer como uma transição duplicada na timeline — daí o filtro ser obrigatório, não cosmético.

Observações (não são divergência):

  • A validação implementada é espelho exata da que já existe no drawer (DrawerMemory / MEMORY_HIT_FIELDS em OrchestrationDrawer.tsx:275), incluindo o motivo dos 4 campos: type/key/snippet são renderizados como React children.
  • Zod não foi usado: esta rota não valida body (é GET sem query schema) e a leitura é de JSON persistido, coberto pelo safeJsonParse já existente no arquivo. Hard Rule #12 intocada — o catch da rota continua usando sanitizeErrorMessage.
  • Hard Rule #5 respeitada: nenhum SQL na rota; tudo por src/lib/db/a2aTasks.ts.

Trabalho paralelo

src/lib/a2a/taskExecution.ts e tests/unit/a2a-memory-hits.test.ts (Task C2) aparecem como M no git statusnão foram tocados nem adicionados por esta task. O commit contém exclusivamente os 2 arquivos da C1.