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
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.
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.
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
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.
- 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.