diff --git a/.aiox-core/core/ids/circuit-breaker.js b/.aiox-core/core/ids/circuit-breaker.js index 710e10c694..2a8af20fe6 100644 --- a/.aiox-core/core/ids/circuit-breaker.js +++ b/.aiox-core/core/ids/circuit-breaker.js @@ -94,6 +94,11 @@ class CircuitBreaker { */ recordFailure() { this._failureCount++; + + if (this._state === STATE_OPEN) { + return; + } + this._lastFailureTime = Date.now(); if (this._state === STATE_HALF_OPEN) { diff --git a/.aiox-core/data/entity-registry.yaml b/.aiox-core/data/entity-registry.yaml index 40b184ab87..494b7e0a6b 100644 --- a/.aiox-core/data/entity-registry.yaml +++ b/.aiox-core/data/entity-registry.yaml @@ -1,6 +1,6 @@ metadata: version: 1.0.0 - lastUpdated: '2026-05-07T10:44:44.077Z' + lastUpdated: '2026-05-07T13:24:28.327Z' entityCount: 746 checksumAlgorithm: sha256 resolutionRate: 100 @@ -9053,8 +9053,8 @@ entities: score: 0.4 constraints: [] extensionPoints: [] - checksum: sha256:1b35331ba71a6ce17869bab255e087fc540291243f9884fc21ed89f7efc122a4 - lastVerified: '2026-03-11T00:48:55.879Z' + checksum: sha256:63be126f2e0d320daa60cb5b68b21df93cad4c19d1f959e06a6ac776213f8eba + lastVerified: '2026-05-07T13:24:28.323Z' framework-governor: path: .aiox-core/core/ids/framework-governor.js layer: L1 diff --git a/.aiox-core/install-manifest.yaml b/.aiox-core/install-manifest.yaml index ca558521d5..1e8b9efa68 100644 --- a/.aiox-core/install-manifest.yaml +++ b/.aiox-core/install-manifest.yaml @@ -7,8 +7,8 @@ # - SHA256 hashes for change detection # - File types for categorization # -version: 5.1.6 -generated_at: "2026-05-07T12:43:37.364Z" +version: 5.1.7 +generated_at: "2026-05-07T13:26:26.223Z" generator: scripts/generate-install-manifest.js file_count: 1103 files: @@ -697,9 +697,9 @@ files: type: core size: 22865 - path: core/ids/circuit-breaker.js - hash: sha256:1b35331ba71a6ce17869bab255e087fc540291243f9884fc21ed89f7efc122a4 + hash: sha256:63be126f2e0d320daa60cb5b68b21df93cad4c19d1f959e06a6ac776213f8eba type: core - size: 4235 + size: 4295 - path: core/ids/framework-governor.js hash: sha256:ef7a3b7444a51f2f122c114c662b1db544d1c9e7833dc6f77dce30ac3a2005a2 type: core @@ -1229,7 +1229,7 @@ files: type: data size: 9590 - path: data/entity-registry.yaml - hash: sha256:b79e4aa7eece851a30ad7b3598efc3033fd921f5b0b19dac5843a454ee8eec98 + hash: sha256:39bacf2f05602f8cfa709b67062d5c9bfc1cac232ee68038e14f711ce060499a type: data size: 522429 - path: data/learned-patterns.yaml diff --git a/docs/stories/epic-123/STORY-123.11-circuit-breaker-recovery-timeout.md b/docs/stories/epic-123/STORY-123.11-circuit-breaker-recovery-timeout.md new file mode 100644 index 0000000000..63b809a178 --- /dev/null +++ b/docs/stories/epic-123/STORY-123.11-circuit-breaker-recovery-timeout.md @@ -0,0 +1,39 @@ +# STORY-123.11: Preservar timeout de recuperação do CircuitBreaker em OPEN + +Status: Done + +Issue: #469 +PR relacionado: #618 + +## Contexto + +O issue #469 descreve um bug no `CircuitBreaker`: chamadas repetidas a `recordFailure()` enquanto o circuito já está `OPEN` atualizam `lastFailureTime`, reiniciando indefinidamente a janela de recuperação. Isso impede a transição para `HALF_OPEN` enquanto as falhas continuam chegando. + +## Acceptance Criteria + +- [x] AC1. Falhas registradas enquanto o circuito está `OPEN` continuam incrementando `failureCount`. +- [x] AC2. Falhas registradas enquanto o circuito está `OPEN` não atualizam `lastFailureTime`. +- [x] AC3. O circuito transiciona para `HALF_OPEN` após o timeout original, mesmo com falhas subsequentes durante `OPEN`. +- [x] AC4. Há teste automatizado determinístico cobrindo a regressão do issue #469. +- [x] AC5. A correção é publicada como patch `5.1.7`. + +## Tasks + +- [x] Ajustar `recordFailure()` para preservar o marco temporal original quando o estado já é `OPEN`. +- [x] Adicionar regressão na suíte de verification gates. +- [x] Atualizar versão e manifesto de instalação. +- [x] Rodar gates locais antes do PR. + +## Dev Notes + +- O PR #618 continha a intenção correta, mas estava atrás da `main` e sem a bateria atual de validação; a correção foi reaplicada em branch nova baseada na `main`. +- O comportamento em `CLOSED` e `HALF_OPEN` permanece igual: falhas nesses estados ainda atualizam `lastFailureTime`. + +## File List + +- [docs/stories/epic-123/STORY-123.11-circuit-breaker-recovery-timeout.md](./STORY-123.11-circuit-breaker-recovery-timeout.md) +- [.aiox-core/core/ids/circuit-breaker.js](../../../.aiox-core/core/ids/circuit-breaker.js) +- [tests/core/ids/verification-gates.test.js](../../../tests/core/ids/verification-gates.test.js) +- [.aiox-core/install-manifest.yaml](../../../.aiox-core/install-manifest.yaml) +- [package.json](../../../package.json) +- [package-lock.json](../../../package-lock.json) diff --git a/package-lock.json b/package-lock.json index efcac54d37..132beb4a5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@aiox-squads/core", - "version": "5.1.6", + "version": "5.1.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@aiox-squads/core", - "version": "5.1.6", + "version": "5.1.7", "license": "MIT", "workspaces": [ "packages/*" diff --git a/package.json b/package.json index bb317c2703..0322a9b849 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@aiox-squads/core", - "version": "5.1.6", + "version": "5.1.7", "description": "Synkra AIOX: AI-Orchestrated System for Full Stack Development - Core Framework", "bin": { "aiox": "bin/aiox.js", diff --git a/tests/core/ids/verification-gates.test.js b/tests/core/ids/verification-gates.test.js index 2cfc4b3951..9aa497e28d 100644 --- a/tests/core/ids/verification-gates.test.js +++ b/tests/core/ids/verification-gates.test.js @@ -189,6 +189,41 @@ describe('CircuitBreaker', () => { expect(cb.getState()).toBe(STATE_HALF_OPEN); }); + it('preserves the original recovery timeout when failures continue while open', () => { + let now = 1000; + const nowSpy = jest.spyOn(Date, 'now').mockImplementation(() => now); + + try { + const breaker = new CircuitBreaker({ + failureThreshold: 2, + successThreshold: 1, + resetTimeoutMs: 1000, + }); + + breaker.recordFailure(); + now = 1100; + breaker.recordFailure(); + + expect(breaker.getState()).toBe(STATE_OPEN); + expect(breaker.getStats().lastFailureTime).toBe(1100); + + now = 1500; + breaker.recordFailure(); + + expect(breaker.getStats().failureCount).toBe(3); + expect(breaker.getStats().lastFailureTime).toBe(1100); + + now = 2099; + expect(breaker.isAllowed()).toBe(false); + + now = 2100; + expect(breaker.isAllowed()).toBe(true); + expect(breaker.getState()).toBe(STATE_HALF_OPEN); + } finally { + nowSpy.mockRestore(); + } + }); + it('reports correct stats', () => { const stats = cb.getStats(); expect(stats.state).toBe(STATE_OPEN);