Skip to content

fix(dashboard): orchestration canvas fase 3 — follow-ups da review final (#12639) - #12988

Merged
diegosouzapw merged 7 commits into
release/v3.8.51from
feat/orch-fase3-c
Sep 10, 2026
Merged

diegosouzapw merged 7 commits into
release/v3.8.51from
feat/orch-fase3-c

Conversation

@diegosouzapw

@diegosouzapw diegosouzapw commented Sep 7, 2026 •

Copy link
Copy Markdown
Collaborator

Fase 3 do Orchestration Canvas, PR-C: os follow-ups da review final da Fase 2, issue #12639. Cinco dos seis itens; o sexto fica fora por decisão de escopo, explicado abaixo e registrado na issue.

Spec: _tasks/superpowers/specs/2026-09-03-orchestration-canvas-fase3-design.md · Plano: _tasks/superpowers/plans/2026-09-03-orchestration-canvas-fase3.md.

1. History hidrata memoryHits (da801444)

reconstituteHistoricalTask devolvia metadata: {} fixo e nunca lia o evento memory_hits persistido, então a seção "Memória" do drawer só existia dentro da janela de TTL em memória. Agora o data_json do evento é parseado para metadata.memoryHits, com a mesma validação defensiva que o DrawerMemory aplica: Array.isArray, e cada item precisa ter id/key/type/snippet como string. Payload malformado degrada para metadata: {} e HTTP 200 — nunca 500.

Defeito visível que apareceu ao implementar: sem filtrar o memory_hits da lista de eventos, ele caía no else do STATE_EVENT_PREFIX e herdava row.state, aparecendo na timeline como uma transição de estado duplicada. O filtro conserta isso, não é só arrumação.

2. Deadline próprio no recall de memória (3ef0aef1)

collectMemoryHits não tinha deadline nenhum e roda ANTES do handler da skill — um backend HTTP lento (o genericBackend sozinho tem default de 30s) atrasava o início da task. Agora corre contra MEMORY_RECALL_TIMEOUT_MS = 1500 num Promise.race, com clearTimeout num finally nos dois caminhos. Contrato best-effort intacto: estouro ⇒ lista vazia, a task segue, nada propaga.

Achado do mutation-check: a primeira versão tinha timer.unref?.() junto do clearTimeout. Removendo o clearTimeout para verificar, o teste do "timer pendurado" continuava passando — process.getActiveResourcesInfo() não reporta timer unrefado, ou seja o unref mascarava exatamente o defeito que o teste deveria pegar. O unref saiu; refeito o mutation-check, o teste agora falha sem o clearTimeout.

3. requirements.cli/model no repeat do Conductor · 4. foco na task nova · 5. teste de auth (a7d430fe)

hubTaskSchema passou a parsear requirements e ConductorTaskDetail ganhou cli/model (null quando o hub não envia), então repetir uma execução do Conductor a fixa no mesmo perfil de runner e modelo em vez de deixá-la derivar para o que a frota escolher.

performAction passou a devolver o corpo já parseado, e newNodeIdFrom monta o id no formato que o canvas indexa — conductor:task:<id>, cloud-agent:<id>, a2a:<id>. Um repeat bem-sucedido a partir da aba Agents foca a execução criada; da aba History o comportamento fica como está (as execuções de lá não são endereçáveis no snapshot vivo). approve/cancel seguem devolvendo boolean de propósito: a resposta deles ecoa o id da MESMA task e navegaria para onde já se está.

O teste de auth de POST /api/conductor/tasks entrou no array ROUTES compartilhado (regex do proxy estendido com createConductorTask\() e a duplicata saiu de conductor-create-route.test.ts. As quatro asserções são as mesmas — cobertura preservada, não enfraquecida.

Divergências entre plano e código, resolvidas pelo código

  • O plano mandava setParams({ node: <id> }) com o id cru. O canvas indexa por id prefixado — daí o newNodeIdFrom. O caso a2a: nem estava no plano; o repeat do drawer também atende A2A (envelope result.task.id).
  • repeatReqForConductor passa undefined, não null, quando o campo falta: o Zod real da rota é z.string().optional() e null daria 400. Mesma classe de erro que a Fase 2 cometeu ao parafrasear contrato em vez de abrir o arquivo.
  • .catch(null) no requirements, que o plano não pedia: o mesmo schema parseia a lista em getFleetSnapshot, então um requirements de formato inesperado numa única task derrubaria o snapshot inteiro para offline.
  • A mudança em conductor-routes-auth.test.ts não falhava antes — é de-duplicação e ampliação de cobertura, não prova de defeito. Registrado para não passar por TDD o que não é.

Fora de escopo, com motivo

O sexto item da issue — caminho de criação A2A autenticado pelo painel — fica fora. O endpoint JSON-RPC aceita apenas chave de API, e alargar a superfície de autenticação dele para resolver uma conveniência de UI é decisão do operador, não da implementação. Está no spec (§4) e comentado na issue; segue como limitação documentada no changelog da Fase 2.

Gates

typecheck:core limpo · check:dashboard-typecheck OK (206 erros pré-existentes, dentro do baseline) · check:cycles OK · client-bundle-no-server-only-10692 1/1 · vitest tests/unit/ui/ 210 arquivos / 1200 testes · suítes a2a + conductor 44/44.

⚠️ base-red inherited: #12732

Os vermelhos abaixo vêm do tip de release/v3.8.51, não desta PR — nenhum dos arquivos envolvidos é tocado aqui.

Vermelho Causa na base Fix em voo
API Route Typecheck open-sse/executors/glm.ts TS2554 #12925
Merge integrity um SKILL.md gerado desatualizado (Generated: 1 · Unchanged: 45) #12923
Docs Gates / check:docs-all migrations 169→171 e "19 routing strategies"→20 #12959
i18n:check 30 fontes de docs/ mudaram desde a última tradução —
tests/unit/zai-web-silent-empty-repro.test.ts código de erro virou bad_gateway sem atualizar o teste —
aviso de supressões órfãs no lint entradas mortas em open-sse/services/combo.ts, tlsClientBase.ts, useProviderConnections.ts, catalogCache.ts —

Closes #12639


Atualização sobre os vermelhos herdados (reproduzidos no tip limpo): as quatro shards de Unit Tests fast-path também estão vermelhas na base. Prova de isolamento: a #12983 toca só UI do dashboard, i18n e changelog, e apresenta exatamente o mesmo conjunto de falhas da #12988, que toca rotas A2A/Conductor. Rodando no checkout principal em d6f31501, sem nenhuma alteração aplicada:

node --import tsx/esm --test tests/unit/chatcore-translation-paths.test.ts
✖ chatCore locks per-model quota failures without dropping quota helper references
  AssertionError: false !== true   (tests/unit/chatcore-translation-paths.test.ts:3249)

Inventário por shard e o resto da evidência estão em #12732 (comment). Nenhum dos arquivos envolvidos é tocado por esta PR.

`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
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
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
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
…fase3-c

# Conflicts:
#	src/app/(dashboard)/dashboard/orchestration/OrchestrationPageClient.tsx
#	src/app/(dashboard)/dashboard/orchestration/drawer/useDrawerDetail.ts
@diegosouzapw
diegosouzapw merged commit 5905827 into release/v3.8.51 Sep 10, 2026
8 of 11 checks passed
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…nal (cheaper-inference#12639) (cheaper-inference#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 cheaper-inference#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 cheaper-inference#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 cheaper-inference#12639

* chore(a2a): follow-ups changelog

Changelog fragment for the five items PR-C delivers from cheaper-inference#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 cheaper-inference#12639
@diegosouzapw
diegosouzapw deleted the feat/orch-fase3-c branch October 18, 2026 23:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

chore(dashboard): follow-ups da review final da PR-C (Repetir + memoryHits)

1 participant