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
5 changes: 5 additions & 0 deletions .aiox-core/core/ids/circuit-breaker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
6 changes: 3 additions & 3 deletions .aiox-core/data/entity-registry.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions .aiox-core/install-manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
35 changes: 35 additions & 0 deletions tests/core/ids/verification-gates.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading