fix(core/server): captura perdida encerra a direcao e running para de anunciar direcao morta (#62) - #63
fix(core/server): captura perdida encerra a direcao e running para de anunciar direcao morta (#62)#63caioross wants to merge 4 commits into
Conversation
…zumbi (#62) Esgotado o orcamento de CAPTURE_MAX_RETRIES, `_capture` emitia `error.capture_lost` e saia SEM setar `_stop`: segmentador, laco de traducao e os `_OutputSink` seguiam girando para sempre com a fila vazia — app "vivo", nada traduzindo, devices de saida presos e o Parar desabilitado na UI (o evento nao e recuperavel). E a descricao literal do zumbi que a #45 corrigiu no laco de traducao, na porta ao lado. Agora `_stop.set()` vem ANTES do evento, como no caminho fatal do `_translation_loop`. O `finally` de `_run` tambem passa a setar `_stop`: rede final que cobre a falha de SETUP (modelo que nao carrega), que nao passa pelo laco e ate agora saia com `_stop` limpo. `is_alive()` expoe esse estado para o servidor sem que ele leia `_stop`. Cobertura em tests_unit/test_capture_retry.py (sem sounddevice, modelo ou device real): captura perdida seta `_stop`, falha transitoria NAO encerra a direcao, stop do usuario nao vira erro e falha de setup tambem mata a direcao.
`_workers` so perdia entrada no `/api/stop`. Uma direcao que morre por conta propria (captura perdida, falhas consecutivas, falha de setup) continuava listada, e o `hello` do WS respondia com ela em `running`: um F5 — reacao natural de quem acabou de ver um erro — repintava o painel de verde "Em execucao", com o Parar habilitado, sobre uma direcao que nao traduz mais. O usuario fala e nao sai nada, sem nenhuma pista na tela. `_running_directions()` filtra por `is_alive()` e despeja o worker morto do dicionario; `/api/status` e o `hello` passam a usa-la. O formato da resposta nao muda (lista de direcoes) — so para de mentir. Nao chama `stop()` no despejo: o worker morto ja fechou os proprios sinks no `finally` de `_run` e `stop()` faz join de ate 2s por thread, caro demais para um handler async. Cobertura em tests_unit/test_running_directions.py.
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
O quorum adversarial vetou o despejo: `_running_directions` fazia
`_workers.pop()` para o worker com `is_alive() == False`, e isso joga fora a
UNICA referencia ao worker num instante em que ele AINDA segura device.
`_stop` e setado no comeco do `finally` de `_run`; `_close_sinks()` so roda
depois que o laco de traducao retorna — pode levar segundos (`_play` espera ate
`len(pcm)/sr + OUT_SINK_WAIT_MARGIN_S`) ou nunca, se um engine pendurar.
Sem a referencia, o `/api/stop` seguinte cai em `pop(..., None) is None` e
devolve `{"ok": true}` sem chamar `stop()`, e o `existing.stop()` do
`/api/start` e pulado: o device de saida fica preso ate o processo morrer e o
start novo reabre o mesmo device por cima do antigo. Era regressao direta da
doutrina da #38 ("parar/reiniciar nao pode vazar device").
Filtrar resolve a #62 igual e nao muta estado: `_stop` nunca e limpo, entao
`is_alive()` e monotonico e a direcao morta jamais reaparece em `running`.
Quem solta o worker do dicionario continua sendo `/api/stop` ou o proximo
start. De quebra, `/api/status` volta a ser um GET sem efeito colateral.
Testes ajustados para afirmar a invariante certa (a referencia sobrevive ao
despejo da lista) + um novo cobrindo que consultar `running` nao muta nada.
…t caido (#62) O quorum apontou que a correcao do backend nao chegava ao usuario no caminho sem F5. O handler de `hello` em app.js era ADITIVO: iterava so sobre `ev.running` e so sabia LIGAR painel. Como o WS reconecta sozinho (`onclose` → `scheduleReconnect` → `connectWS`), o `hello` corrigido era descartado justamente quando era a unica fonte de verdade. Cenario: aba aberta com FALAR rodando, o socket cai, o mic some e a captura morre durante a queda. O `error.capture_lost` e transmitido para um socket morto e a UI nunca o ve. O WS volta, o `hello` chega com `running: []` — e o painel continua verde "Em execucao" com o Parar habilitado sobre uma direcao que nao traduz ha minutos. Era o sintoma da #62 por outra porta. Agora o `hello` e autoritativo nos dois sentidos. Desliga apenas o que o cliente ACHAVA que estava rodando (`state.running`): painel em erro ja saiu de `state.running` e mantem o erro na tela, e start otimista ainda nao entrou — nao sofre stomp com o POST em voo. Sem chave nova de i18n (`status.idle` e `status.running` ja existem em PT e EN).
Parecer do PR Doctor — quórum §7.2, duas rodadas
Resultado: 2× APROVA, 1× VETO confirmado. Sem 3× APROVA, não mergeio. Pela §7.2 (veto persiste após um reparo e uma re-convocação), a PR vai para DRAFT + Rodada 1 — 3× VETOAs três lentes vetaram, com dois defeitos distintos e reais: (a) Pipeline/Latência e Privacidade/Robustez, convergentes: o (b) Produto/UX: o handler de Reparo aplicado (
|
|
Nota do PR Doctor (não muda o status desta PR — segue DRAFT +
Ou seja: o que falta decidir aqui ganhou um vizinho concreto. A pergunta de produto que trava as duas é a mesma — perder 1 de N saídas deve matar a direção inteira, ou ela segue degradada com aviso persistente? Enquanto isso não for decidido, |
Contexto
A #45 estabeleceu a doutrina: falha isolada nao mata a direcao, e a morte de verdade seta
_stopantes de sair, para nao deixar thread zumbi. A #62 aponta os caminhos vizinhos que ainda nao a seguiam. O Curador reduziu o escopo desta issue aos itens 1 e 3 — o item 2 (error.playrecuperavel) e da #54, e nao esta aqui.O que mudou
Item 1 —
laguna_core.py, captura perdida (_capture)Esgotado o orcamento de
CAPTURE_MAX_RETRIES, o laco emitiaerror.capture_loste saia sem setar_stop: segmentador, laco de traducao e os_OutputSinkseguiam girando para sempre com a fila vazia — app "vivo", nada traduzindo, devices de saida presos, e o Parar desabilitado na UI (o evento nao e recuperavel). Agoraself._stop.set()vem antes do evento, igual ao caminho fatal do_translation_loop.Junto, uma linha de rede final: o
finallyde_runtambem seta_stop. Isso cobre a falha de setup (modelo que nao carrega) — caminho que nunca chega ao laco de traducao e ate agora saia com_stoplimpo, deixando um worker morto que o servidor seguia listando. Sem esse pedaco, o item 3 continuaria mentindo nesse cenario.Item 3 —
laguna_server.py,_workersque nao solta direcao morta_workersso perdia entrada no/api/stop. Direcao que morre sozinha continuava listada e ohellodo WS respondia com ela emrunning: um F5 repintava o painel de verde "Em execucao", com o Parar habilitado, sobre uma direcao que nao traduz mais._running_directions()filtra poris_alive()e despeja o worker morto;/api/statuse ohellopassam a usa-la.Contrato REST/WS inalterado:
runningcontinua sendo uma lista de nomes de direcao — so para de incluir direcao morta. Nenhuma mudanca emstatic/.Decisoes de projeto:
is_alive()publico emDirectionWorkerpara o servidor nao ler_stop(privado) de fora.stop(): o worker morto ja fechou os proprios sinks nofinallyde_run, estop()fazjoinde ate 2s por thread — caro demais para um handler async.Gate (HANDBOOK §6) — resultado real
python -m compileall -q .→COMPILE_OK;import fase0_poc, laguna_core, laguna_server→IMPORTS_OK(C:\Python313\python.exe).laguna_core.py).test_offline.py dry_pt2en.wav --direction pt2en --model small --device auto: rodou emcuda/float16,out_gate.wavgerado. STT 1069ms / MT 929ms / TTS 4730ms — cold start da worktree (primeiro uso baixou os modelos); nao ha numero de latencia comparavel aqui e nenhuma constante de VAD/latencia ou default de modelo foi tocada, entao nao se aplica o benchmark antes/depois.static/.pytest tests_unit/ -q→ 63 passed (55 antes + 8 novos). Verificado que os testes novos pegam a regressao: com o diff delaguna_core.py/laguna_server.pyrevertido,test_captura_perdida_seta_stop_e_emite_erro_terminaletest_falha_de_setup_tambem_encerra_a_direcaofalham.Novos:
tests_unit/test_capture_retry.py(captura perdida seta_stop; falha transitoria nao encerra a direcao; stop do usuario nao vira erro; falha de setup tambem mata) etests_unit/test_running_directions.py(viva fica, morta some dorunninge do dicionario, irma viva sobrevive,stop()nao e chamado no despejo). Sem sounddevice, modelo, GPU ou HTTP real — rodam no jobtests-unitda CI.Riscos
_stopnofinallyde_rune o mais abrangente do diff: passa a valer para todo caminho de saida, inclusive a falha de setup. Emstop()e no fatal do laco_stopja estava setado — nao muda nada la. O efeito novo e so: worker que acabou passa a se declarar morto._stopnoexceptgenerico em vez de dentro doif attempt >= CAPTURE_MAX_RETRIES). Coberto por teste dedicado.error.capture_lostcomo terminal (comportamento atual, correto — agora o backend de fato parou). O que muda e o F5: passa a mostrar a direcao como parada, que e a verdade.Solicito quórum (HANDBOOK §7)
Closes #62