Skip to content

Commit cf215e9

Browse files
committed
fix(execution): corrige revisao CodeRabbit no time-travel engine
- Protege emit('error') com listenerCount para evitar throw sem listener - Substitui JSON.parse/stringify por structuredClone com fallback JSON - Adiciona carregamento sincrono de timelines no construtor (_loadFromDiskSync) - Compara timelines por linhagem (parentId) em vez de estado serializado - Usa comprimento total de checkpoints ao inves de contagem de ativos para o limite - Corrige callback forEach para compatibilidade com Biome lint - Adiciona teste para falha de persistencia sem listener de erro
1 parent b4b6000 commit cf215e9

4 files changed

Lines changed: 205 additions & 65 deletions

File tree

.aios-core/core/execution/time-travel.js

Lines changed: 84 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ function generateId(prefix) {
5353
*/
5454
function deepClone(value) {
5555
if (value === null || value === undefined) return value;
56+
if (typeof structuredClone === 'function') return structuredClone(value);
5657
return JSON.parse(JSON.stringify(value));
5758
}
5859

@@ -84,13 +85,20 @@ class TimeTravelEngine extends EventEmitter {
8485
/** @type {Map<string, Object>} */
8586
this.timelines = new Map();
8687

88+
this._loaded = false;
89+
8790
this._stats = {
8891
timelinesCreated: 0,
8992
checkpointsCreated: 0,
9093
forksCreated: 0,
9194
rewindsPerformed: 0,
9295
restoresPerformed: 0,
9396
};
97+
98+
// Sync load persisted timelines on startup
99+
if (this.autoPersist) {
100+
this._loadFromDiskSync();
101+
}
94102
}
95103

96104
// ---------------------------------------------------------------------------
@@ -152,11 +160,7 @@ class TimeTravelEngine extends EventEmitter {
152160
async checkpoint(timelineId, state, label = '') {
153161
const timeline = this._getTimeline(timelineId);
154162

155-
const activeCount = timeline.checkpoints.filter(
156-
(cp) => cp.status === CheckpointStatus.ACTIVE
157-
).length;
158-
159-
if (activeCount >= this.maxCheckpointsPerTimeline) {
163+
if (timeline.checkpoints.length >= this.maxCheckpointsPerTimeline) {
160164
throw new Error(
161165
`Timeline has reached maximum of ${this.maxCheckpointsPerTimeline} checkpoints`
162166
);
@@ -169,7 +173,7 @@ class TimeTravelEngine extends EventEmitter {
169173
state: deepClone(state),
170174
label: label ?? '',
171175
timestamp: new Date().toISOString(),
172-
index: activeCount,
176+
index: timeline.checkpoints.length,
173177
status: CheckpointStatus.ACTIVE,
174178
};
175179

@@ -395,29 +399,49 @@ class TimeTravelEngine extends EventEmitter {
395399
const tl1 = this._getTimeline(timelineId1);
396400
const tl2 = this._getTimeline(timelineId2);
397401

398-
// Find shared checkpoints by comparing state content
402+
// Find shared checkpoints using lineage (parent relationship)
399403
const sharedCheckpoints = [];
400404
let commonAncestorIndex = -1;
401-
402-
const minLen = Math.min(tl1.checkpoints.length, tl2.checkpoints.length);
403-
for (let i = 0; i < minLen; i++) {
404-
const s1 = JSON.stringify(tl1.checkpoints[i].state);
405-
const s2 = JSON.stringify(tl2.checkpoints[i].state);
406-
407-
if (s1 === s2) {
408-
sharedCheckpoints.push({
409-
index: i,
410-
label: tl1.checkpoints[i].label,
411-
state: deepClone(tl1.checkpoints[i].state),
412-
});
413-
commonAncestorIndex = i;
414-
} else {
415-
break;
405+
let forkPointIndex = 0;
406+
407+
// Check if tl2 is a fork of tl1 (or vice versa)
408+
if (tl2.parentId === timelineId1) {
409+
// tl2 was forked from tl1 — use parentCheckpointId to find fork point
410+
const forkCpId = tl2.parentCheckpointId;
411+
const forkCpIdx = tl1.checkpoints.findIndex((cp) => cp.id === forkCpId);
412+
413+
if (forkCpIdx >= 0) {
414+
for (let i = 0; i <= forkCpIdx; i++) {
415+
sharedCheckpoints.push({
416+
index: i,
417+
label: tl1.checkpoints[i].label,
418+
state: deepClone(tl1.checkpoints[i].state),
419+
});
420+
commonAncestorIndex = i;
421+
}
422+
forkPointIndex = forkCpIdx + 1;
423+
}
424+
} else if (tl1.parentId === timelineId2) {
425+
// tl1 was forked from tl2 — use parentCheckpointId to find fork point
426+
const forkCpId = tl1.parentCheckpointId;
427+
const forkCpIdx = tl2.checkpoints.findIndex((cp) => cp.id === forkCpId);
428+
429+
if (forkCpIdx >= 0) {
430+
for (let i = 0; i <= forkCpIdx; i++) {
431+
sharedCheckpoints.push({
432+
index: i,
433+
label: tl2.checkpoints[i].label,
434+
state: deepClone(tl2.checkpoints[i].state),
435+
});
436+
commonAncestorIndex = i;
437+
}
438+
forkPointIndex = forkCpIdx + 1;
416439
}
417440
}
441+
// For unrelated timelines, sharedCheckpoints stays empty
418442

419443
// Divergent checkpoints
420-
const onlyInTimeline1 = tl1.checkpoints.slice(sharedCheckpoints.length).map((cp) => ({
444+
const onlyInTimeline1 = tl1.checkpoints.slice(forkPointIndex).map((cp) => ({
421445
checkpointId: cp.id,
422446
label: cp.label,
423447
index: cp.index,
@@ -645,19 +669,50 @@ class TimeTravelEngine extends EventEmitter {
645669
const filePath = path.join(this.storageDir, `${timeline.id}.json`);
646670
await fs.promises.writeFile(filePath, JSON.stringify(timeline, null, 2), 'utf-8');
647671
} catch (error) {
648-
this.emit('error', {
649-
operation: 'persist',
650-
timelineId: timeline.id,
651-
error: error.message,
652-
});
672+
if (this.listenerCount('error') > 0) {
673+
this.emit('error', {
674+
operation: 'persist',
675+
timelineId: timeline.id,
676+
error: error.message,
677+
});
678+
}
679+
}
680+
}
681+
682+
/**
683+
* Load all timelines from disk (sync, used by constructor)
684+
* @private
685+
*/
686+
_loadFromDiskSync() {
687+
if (this._loaded) return;
688+
try {
689+
const files = fs.readdirSync(this.storageDir);
690+
const jsonFiles = files.filter((f) => f.endsWith('.json'));
691+
692+
for (const file of jsonFiles) {
693+
try {
694+
const filePath = path.join(this.storageDir, file);
695+
const data = fs.readFileSync(filePath, 'utf-8');
696+
const timeline = JSON.parse(data);
697+
if (timeline.id) {
698+
this.timelines.set(timeline.id, timeline);
699+
}
700+
} catch (_err) {
701+
// Skip corrupt files
702+
}
703+
}
704+
} catch (_err) {
705+
// Directory may not exist yet
653706
}
707+
this._loaded = true;
654708
}
655709

656710
/**
657711
* Load all timelines from disk
658712
* @private
659713
*/
660714
async _loadFromDisk() {
715+
if (this._loaded) return;
661716
try {
662717
const files = await fs.promises.readdir(this.storageDir);
663718
const jsonFiles = files.filter((f) => f.endsWith('.json'));
@@ -677,6 +732,7 @@ class TimeTravelEngine extends EventEmitter {
677732
} catch (_err) {
678733
// Directory may not exist yet
679734
}
735+
this._loaded = true;
680736
}
681737
}
682738

.aiox-core/core/execution/time-travel.js

Lines changed: 84 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ function generateId(prefix) {
5353
*/
5454
function deepClone(value) {
5555
if (value === null || value === undefined) return value;
56+
if (typeof structuredClone === 'function') return structuredClone(value);
5657
return JSON.parse(JSON.stringify(value));
5758
}
5859

@@ -84,13 +85,20 @@ class TimeTravelEngine extends EventEmitter {
8485
/** @type {Map<string, Object>} */
8586
this.timelines = new Map();
8687

88+
this._loaded = false;
89+
8790
this._stats = {
8891
timelinesCreated: 0,
8992
checkpointsCreated: 0,
9093
forksCreated: 0,
9194
rewindsPerformed: 0,
9295
restoresPerformed: 0,
9396
};
97+
98+
// Sync load persisted timelines on startup
99+
if (this.autoPersist) {
100+
this._loadFromDiskSync();
101+
}
94102
}
95103

96104
// ---------------------------------------------------------------------------
@@ -152,11 +160,7 @@ class TimeTravelEngine extends EventEmitter {
152160
async checkpoint(timelineId, state, label = '') {
153161
const timeline = this._getTimeline(timelineId);
154162

155-
const activeCount = timeline.checkpoints.filter(
156-
(cp) => cp.status === CheckpointStatus.ACTIVE
157-
).length;
158-
159-
if (activeCount >= this.maxCheckpointsPerTimeline) {
163+
if (timeline.checkpoints.length >= this.maxCheckpointsPerTimeline) {
160164
throw new Error(
161165
`Timeline has reached maximum of ${this.maxCheckpointsPerTimeline} checkpoints`
162166
);
@@ -169,7 +173,7 @@ class TimeTravelEngine extends EventEmitter {
169173
state: deepClone(state),
170174
label: label ?? '',
171175
timestamp: new Date().toISOString(),
172-
index: activeCount,
176+
index: timeline.checkpoints.length,
173177
status: CheckpointStatus.ACTIVE,
174178
};
175179

@@ -395,29 +399,49 @@ class TimeTravelEngine extends EventEmitter {
395399
const tl1 = this._getTimeline(timelineId1);
396400
const tl2 = this._getTimeline(timelineId2);
397401

398-
// Find shared checkpoints by comparing state content
402+
// Find shared checkpoints using lineage (parent relationship)
399403
const sharedCheckpoints = [];
400404
let commonAncestorIndex = -1;
401-
402-
const minLen = Math.min(tl1.checkpoints.length, tl2.checkpoints.length);
403-
for (let i = 0; i < minLen; i++) {
404-
const s1 = JSON.stringify(tl1.checkpoints[i].state);
405-
const s2 = JSON.stringify(tl2.checkpoints[i].state);
406-
407-
if (s1 === s2) {
408-
sharedCheckpoints.push({
409-
index: i,
410-
label: tl1.checkpoints[i].label,
411-
state: deepClone(tl1.checkpoints[i].state),
412-
});
413-
commonAncestorIndex = i;
414-
} else {
415-
break;
405+
let forkPointIndex = 0;
406+
407+
// Check if tl2 is a fork of tl1 (or vice versa)
408+
if (tl2.parentId === timelineId1) {
409+
// tl2 was forked from tl1 — use parentCheckpointId to find fork point
410+
const forkCpId = tl2.parentCheckpointId;
411+
const forkCpIdx = tl1.checkpoints.findIndex((cp) => cp.id === forkCpId);
412+
413+
if (forkCpIdx >= 0) {
414+
for (let i = 0; i <= forkCpIdx; i++) {
415+
sharedCheckpoints.push({
416+
index: i,
417+
label: tl1.checkpoints[i].label,
418+
state: deepClone(tl1.checkpoints[i].state),
419+
});
420+
commonAncestorIndex = i;
421+
}
422+
forkPointIndex = forkCpIdx + 1;
423+
}
424+
} else if (tl1.parentId === timelineId2) {
425+
// tl1 was forked from tl2 — use parentCheckpointId to find fork point
426+
const forkCpId = tl1.parentCheckpointId;
427+
const forkCpIdx = tl2.checkpoints.findIndex((cp) => cp.id === forkCpId);
428+
429+
if (forkCpIdx >= 0) {
430+
for (let i = 0; i <= forkCpIdx; i++) {
431+
sharedCheckpoints.push({
432+
index: i,
433+
label: tl2.checkpoints[i].label,
434+
state: deepClone(tl2.checkpoints[i].state),
435+
});
436+
commonAncestorIndex = i;
437+
}
438+
forkPointIndex = forkCpIdx + 1;
416439
}
417440
}
441+
// For unrelated timelines, sharedCheckpoints stays empty
418442

419443
// Divergent checkpoints
420-
const onlyInTimeline1 = tl1.checkpoints.slice(sharedCheckpoints.length).map((cp) => ({
444+
const onlyInTimeline1 = tl1.checkpoints.slice(forkPointIndex).map((cp) => ({
421445
checkpointId: cp.id,
422446
label: cp.label,
423447
index: cp.index,
@@ -645,19 +669,50 @@ class TimeTravelEngine extends EventEmitter {
645669
const filePath = path.join(this.storageDir, `${timeline.id}.json`);
646670
await fs.promises.writeFile(filePath, JSON.stringify(timeline, null, 2), 'utf-8');
647671
} catch (error) {
648-
this.emit('error', {
649-
operation: 'persist',
650-
timelineId: timeline.id,
651-
error: error.message,
652-
});
672+
if (this.listenerCount('error') > 0) {
673+
this.emit('error', {
674+
operation: 'persist',
675+
timelineId: timeline.id,
676+
error: error.message,
677+
});
678+
}
679+
}
680+
}
681+
682+
/**
683+
* Load all timelines from disk (sync, used by constructor)
684+
* @private
685+
*/
686+
_loadFromDiskSync() {
687+
if (this._loaded) return;
688+
try {
689+
const files = fs.readdirSync(this.storageDir);
690+
const jsonFiles = files.filter((f) => f.endsWith('.json'));
691+
692+
for (const file of jsonFiles) {
693+
try {
694+
const filePath = path.join(this.storageDir, file);
695+
const data = fs.readFileSync(filePath, 'utf-8');
696+
const timeline = JSON.parse(data);
697+
if (timeline.id) {
698+
this.timelines.set(timeline.id, timeline);
699+
}
700+
} catch (_err) {
701+
// Skip corrupt files
702+
}
703+
}
704+
} catch (_err) {
705+
// Directory may not exist yet
653706
}
707+
this._loaded = true;
654708
}
655709

656710
/**
657711
* Load all timelines from disk
658712
* @private
659713
*/
660714
async _loadFromDisk() {
715+
if (this._loaded) return;
661716
try {
662717
const files = await fs.promises.readdir(this.storageDir);
663718
const jsonFiles = files.filter((f) => f.endsWith('.json'));
@@ -677,6 +732,7 @@ class TimeTravelEngine extends EventEmitter {
677732
} catch (_err) {
678733
// Directory may not exist yet
679734
}
735+
this._loaded = true;
680736
}
681737
}
682738

.aiox-core/install-manifest.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
# - File types for categorization
99
#
1010
version: 5.0.3
11-
generated_at: "2026-03-11T02:25:32.615Z"
11+
generated_at: "2026-03-11T02:25:38.874Z"
1212
generator: scripts/generate-install-manifest.js
1313
file_count: 1090
1414
files:
@@ -437,9 +437,9 @@ files:
437437
type: core
438438
size: 25738
439439
- path: core/execution/time-travel.js
440-
hash: sha256:535683b7d272798246e526bde570535fa20a694fcf38fc4e5caa6039e3c36bfc
440+
hash: sha256:eb133cf60cb26c7771c07bfbc4809872d688f94abdbee995e14a7d4efc379da0
441441
type: core
442-
size: 19783
442+
size: 21615
443443
- path: core/execution/wave-executor.js
444444
hash: sha256:4e2324edb37ae0729062b5ac029f2891e050e7efd3a48d0f4a1dc4f227a6716b
445445
type: core
@@ -1225,9 +1225,9 @@ files:
12251225
type: data
12261226
size: 9575
12271227
- path: data/entity-registry.yaml
1228-
hash: sha256:151aca46769c614f0238e7a8e2968884c3888b438ff1c634f0eeb2505e325b83
1228+
hash: sha256:c2b44be42b65aa41e7cf5bd749b75c9356334c5f81b8d5b1defe0816e3b5540a
12291229
type: data
1230-
size: 521804
1230+
size: 522289
12311231
- path: data/learned-patterns.yaml
12321232
hash: sha256:24ac0b160615583a0ff783d3da8af80b7f94191575d6db2054ec8e10a3f945dc
12331233
type: data

0 commit comments

Comments
 (0)