* 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
4.4 KiB
Task C2 — timeout no recall de memória (PR-C, Fase 3, Refs #12639)
Commit: 3ef0aef15be50bf458d3f54078e4c0107533f4b2 — fix(a2a): bound the memory recall with its own deadline
Branch: feat/orch-fase3-c. Arquivos commitados (só estes dois):
src/lib/a2a/taskExecution.tstests/unit/a2a-memory-hits.test.ts
O que mudou
src/lib/a2a/taskExecution.ts:
export const MEMORY_RECALL_TIMEOUT_MS = 1500;MemoryHitsDepsganhoutimeoutMs?: number(defaultMEMORY_RECALL_TIMEOUT_MS).class MemoryRecallTimeoutError extends Error(marcador interno, não exportado) só para ocatchdistinguir deadline de erro de backend.collectMemoryHitsrodaPromise.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 porsrc/lib/a2a/taskManager.ts). O log não contémerr.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):
MEMORY_RECALL_TIMEOUT_MSé 1500.searchque nunca resolve ⇒[](com asserção de que voltou em <1s, provando que o deadline injetado é o que valeu).searchque resolve dentro do prazo ⇒ hits normais.- Nenhum timer fica pendurado nos dois caminhos.
executeA2ATaskWithStatecompleta 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 decollectMemoryHitse o kill-switchOMNIROUTE_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 mesmologger("A2A_TASKS")detaskManager.ts(otaskExecution.tsnão tinha logger algum). - O
unrefsugerido 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 peloclearTimeoutnofinally, agora com asserção que tem dentes. - Trabalho paralelo (C1):
git statusmostrava apenastask-c1-report.mduntracked; não toquei nele nem emsrc/app/api/a2a/tasks/[id]/route.ts/tests/unit/a2a-history-route.test.ts.git addfoi feito nomeando só os meus dois arquivos.