Files
OmniRoute/task-c2-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

4.4 KiB
Raw Blame History

Task C2 — timeout no recall de memória (PR-C, Fase 3, Refs #12639)

Commit: 3ef0aef15be50bf458d3f54078e4c0107533f4b2fix(a2a): bound the memory recall with its own deadline Branch: feat/orch-fase3-c. Arquivos commitados (só estes dois):

  • src/lib/a2a/taskExecution.ts
  • tests/unit/a2a-memory-hits.test.ts

O que mudou

src/lib/a2a/taskExecution.ts:

  • export const MEMORY_RECALL_TIMEOUT_MS = 1500;
  • MemoryHitsDeps ganhou timeoutMs?: number (default MEMORY_RECALL_TIMEOUT_MS).
  • class MemoryRecallTimeoutError extends Error (marcador interno, não exportado) só para o catch distinguir deadline de erro de backend.
  • collectMemoryHits roda Promise.race([search(...), deadline]); estouro ⇒ log.warn + []. O contrato best-effort é idêntico ao de hoje: nada propaga, a task segue.
  • finally { if (timer) clearTimeout(timer); } — limpo nos DOIS caminhos.
  • logger("A2A_TASKS") importado de @omniroute/open-sse/utils/logger (mesmo canal já usado por src/lib/a2a/taskManager.ts). O log não contém err.message/err.stack — só o id da task e o deadline (Hard Rule #12; nada disso vai para resposta nenhuma).

tests/unit/a2a-memory-hits.test.ts (+5 testes, node:test, sem SQLite/deps reais):

  1. MEMORY_RECALL_TIMEOUT_MS é 1500.
  2. search que nunca resolve ⇒ [] (com asserção de que voltou em <1s, provando que o deadline injetado é o que valeu).
  3. search que resolve dentro do prazo ⇒ hits normais.
  4. Nenhum timer fica pendurado nos dois caminhos.
  5. executeA2ATaskWithState completa a task normalmente quando o recall estoura.

Por que o teste é rápido

Deadline injetável via MemoryHitsDeps.timeoutMs (o arquivo de teste já é 100% construído sobre esse DI seam — search/appendEvent fakes, zero fake timers, zero mock de módulo), então o caminho de timeout usa timeoutMs: 5. Suíte inteira em ~0,9 s de relógio.

Decisão de projeto que mudei no meio (importante)

A primeira implementação tinha timer.unref?.() além do clearTimeout. Mutation-check (remover o clearTimeout e rodar) mostrou que o teste do timer pendurado passava mesmo assim: process.getActiveResourcesInfo() não reporta timer unrefado, ou seja o unref mascarava exatamente o defeito que o teste existe para pegar. Tirei o unref — a garantia real é o clearTimeout no finally — e refiz o mutation-check:

--- MUTATED (clearTimeout removed) ---
✖ collectMemoryHits leaves no pending timer behind on either path (20.667659ms)
 pass 15
 fail 1
--- RESTORED ---
✔ collectMemoryHits leaves no pending timer behind on either path (9.073282ms)
 pass 16
 fail 0

Gates (saída real)

C2.2 — vermelho antes da implementação:

SyntaxError: The requested module '../../src/lib/a2a/taskExecution.ts' does not provide an
export named 'MEMORY_RECALL_TIMEOUT_MS'
 pass 0
 fail 1

C2.4 — node --import tsx/esm --test tests/unit/a2a-memory-hits.test.ts:

 tests 16
 pass 16
 fail 0

Todas as suítes a2a (node --import tsx/esm --test $(ls tests/unit/*a2a*.test.ts)):

 tests 106
 suites 2
 pass 106
 fail 0
 duration_ms 12146.220038

npm run typecheck:core — sem saída de erro (só as linhas npm notice), exit 0. npx eslint src/lib/a2a/taskExecution.ts tests/unit/a2a-memory-hits.test.ts — zero achados. npx prettier --write nos dois arquivos (o de teste foi reformatado; re-rodei a suíte depois: 16/16). Hooks do husky rodaram no commit (any-budget PASS, tracked-artifacts OK) — sem --no-verify.

Divergências plano × código

  • Nenhuma no contrato. MemoryHit, MemoryHitsDeps, a assinatura de collectMemoryHits e o kill-switch OMNIROUTE_A2A_MEMORY_HITS === "0" estão exatamente como o plano copiou.
  • Detalhe não previsto: o plano dizia "estouro ⇒ [] + log" sem dizer qual logger. Usei o mesmo logger("A2A_TASKS") de taskManager.ts (o taskExecution.ts não tinha logger algum).
  • O unref sugerido pela minha leitura inicial de "sem handle pendurado segurando o processo do runner" foi descartado por mascarar o teste (ver acima); o requisito continua atendido pelo clearTimeout no finally, agora com asserção que tem dentes.
  • Trabalho paralelo (C1): git status mostrava apenas task-c1-report.md untracked; não toquei nele nem em src/app/api/a2a/tasks/[id]/route.ts / tests/unit/a2a-history-route.test.ts. git add foi feito nomeando só os meus dois arquivos.