Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Versionamento independente do OpenClaw — kit segue semver próprio.
## [Unreleased]

### Adicionado
- **Lesson** [`lessons/2026-06-16-openclaw-6.8-apply-patches-pattern-drift.md`](lessons/2026-06-16-openclaw-6.8-apply-patches-pattern-drift.md) — (sem TL;DR — ver lesson para detalhes)
- **Lesson** [`lessons/2026-06-09-openclaw-6.5-upgrade-patches-not-native-and-backup-retention.md`](lessons/2026-06-09-openclaw-6.5-upgrade-patches-not-native-and-backup-retention.md) — Upgrade planejado 2026.6.5 (primeiro stable do train 6.5, depois de 5 betas). A lição central é o oposto da do upgrade anterior: no 6.1, três monkey-patches viraram correções nativas e foram aposentad
- **Lesson** [`lessons/2026-06-02-eval-saturation-and-7-bugs-cascade.md`](lessons/2026-06-02-eval-saturation-and-7-bugs-cascade.md) — Sessão de 4h45 disparada por agents Discord/WhatsApp travados. Causa imediata: eval do memoria-nox consumindo 395% CPU + 62.7% steal time do hypervisor Hostinger (noisy neighbor). Toto autorizou parar
- **Lesson** [`lessons/2026-06-04-openclaw-6.1-upgrade-patches-native.md`](lessons/2026-06-04-openclaw-6.1-upgrade-patches-native.md) — Upgrade planejado de 2026.5.27 → 2026.6.1 (a 5.28 que estava agendada já havia sido superada; latest no npm = 6.1). Salto de changelog sem nenhuma seção "Breaking". O achado central: os três monkey-pa
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ openclaw-update-toolkit/
│ ├── upgrade-from-v24-to-v29.md
│ ├── recovery-from-fratricide-loop.md
│ └── recovery-from-cost-explosion.md
└── lessons/ # 28 lições por incident — fonte de truth
└── lessons/ # 29 lições por incident — fonte de truth
```

---
Expand Down
70 changes: 70 additions & 0 deletions lessons/2026-06-16-openclaw-6.8-apply-patches-pattern-drift.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Lesson: dist-bundle patches sofrem pattern-drift quando o upstream refatora a linha-alvo (OpenClaw 6.8)

> Data: 2026-06-16 | Contexto: upgrade OpenClaw 6.6 → 6.8 | Severidade: P3 (degradação silenciosa, fail-open)

## O que aconteceu

Carregamos dois patches de dist-bundle no OpenClaw via um script `apply-patches.sh` rodado como `ExecStartPre` do systemd (idempotente, reaplica a cada restart):

1. **empty-response** — preserva texto streamed quando o evento `result` do Claude CLI chega com texto vazio (`claude-live-session-*.js`).
2. **tool-only** — não dispara `FailoverError`/`empty_response` em turns que só fizeram tool calls (`cli-runner-*.js`).

Ambos os patches funcionam por **string-match exato** da linha original do bundle minificado, com fallback de rollback se o match falha.

No upgrade para 6.8, o **empty-response continuou aplicando**, mas o **tool-only falhou silenciosamente**. O log do `apply-patches`:

```
TARGET claude-tool-only: cli=cli-runner-<hash>.js
Patch 2 target not found in cli-runner file
FAIL claude-tool-only: marker missing after patch, restoring backups
END openclaw apply-patches: FAIL failures=1
```

Como o `ExecStartPre` é prefixado com `-` (ignora falha), o gateway subiu normalmente — a falha **não apareceu** em `openclaw --version`, nem no boot log do gateway, nem no health. Só foi pega porque validamos os markers no dist manualmente pós-upgrade.

## Causa-raiz

O 6.8 **reescreveu a linha do `FailoverError`** no `cli-runner`. Forma antiga (que o patch procurava):

```js
if (!assistantText && params.allowEmptyAssistantReplyAsSilent !== true) throw new FailoverError("CLI backend returned an empty response.", {
```

Forma nova no 6.8 (duas mudanças):

```js
if (!assistantText && !output.didSendViaMessagingTool && params.allowEmptyAssistantReplyAsSilent !== true) throw attachCliMessagingDeliveryEvidence(new FailoverError("CLI backend returned an empty response.", {
```

- Adicionou `&& !output.didSendViaMessagingTool` na condição.
- Envolveu o `FailoverError` em `attachCliMessagingDeliveryEvidence(...)`.

Nenhum dos patterns legacy do patch casou → `replacements.find()` retornou `undefined` → "target not found".

## Fix aplicado

Adicionada uma 3ª entrada (em 1ª posição) no array `replacements`, com o pattern do 6.8, inserindo `&& !output.hadToolCalls` na condição nova:

```js
[
'\t\tif (!assistantText && !output.didSendViaMessagingTool && params.allowEmptyAssistantReplyAsSilent !== true) throw attachCliMessagingDeliveryEvidence(new FailoverError("CLI backend returned an empty response.", {',
`\t\tif (!assistantText && !output.hadToolCalls && !output.didSendViaMessagingTool && params.allowEmptyAssistantReplyAsSilent !== true) throw attachCliMessagingDeliveryEvidence(new FailoverError("CLI backend returned an empty response.", { // ${marker}`
],
```

Patterns legacy mantidos como fallback (idempotência cross-version / rollback). Re-rodado → 3/3 OK, restart → ambos markers ativos.

## Lições transferíveis

1. **Pós-upgrade, validar o resultado do patch — não só a versão do binário.** `openclaw --version` verde NÃO significa que os patches pegaram. Conferir SEMPRE `tail /var/log/openclaw-apply-patches.log` por `FAIL`/`failures=`/`status=1` do `ExecStartPre`. Um `ExecStartPre` prefixado com `-` mascara a falha do boot.

2. **String-match exato em bundle minificado é frágil a refactors.** Quanto mais "contexto" a string-âncora carrega (condições extras, wrappers), maior a chance de drift. Manter a âncora mínima e necessária; preferir múltiplos patterns alternativos com `find()`.

3. **Patches downstream são dívida com juros por upgrade.** Cada versão upstream que toca a linha-alvo cobra ~15-30min de re-diagnóstico + adaptação. A saída definitiva é o merge upstream (aqui: PR que resolve empty-response/tool-only na raiz) — enquanto não merga, considerar tornar o matcher robusto (regex tolerante a variações da condição) em vez de string literal.

4. **Falha fail-open ≠ falha invisível.** O design fail-open (gateway sobe mesmo se o patch falha) é correto pra disponibilidade, mas precisa de um alerta ativo (ex.: o `apply-patches.sh` poderia emitir um `notify-discord.sh warn` quando `failures>0`) — senão a degradação fica silenciosa até alguém checar manualmente.

## Ação de follow-up sugerida

- Adicionar alerta Discord no `apply-patches.sh` quando `failures>0` (hoje só loga em arquivo).
- Avaliar tornar o matcher do tool-only um regex tolerante (capturar a condição `if (!assistantText ... throw ... FailoverError("CLI backend returned an empty response.")` independente de cláusulas intermediárias).
Loading