From bb25433baba0f4d1a833549ba99febeded41cd88 Mon Sep 17 00:00:00 2001 From: rafaelscosta Date: Fri, 8 May 2026 03:40:03 -0300 Subject: [PATCH 1/2] fix(synapse): surface layer processing errors --- .aiox-core/core/synapse/engine.js | 23 ++++++++++++-- .../core/synapse/layers/layer-processor.js | 14 ++++++++- .aiox-core/install-manifest.yaml | 10 +++--- tests/synapse/engine.test.js | 29 +++++++++++++++++ tests/synapse/layer-processor.test.js | 31 +++++++++++++++++++ 5 files changed, 99 insertions(+), 8 deletions(-) diff --git a/.aiox-core/core/synapse/engine.js b/.aiox-core/core/synapse/engine.js index 2a019ebe42..6caf13f30c 100644 --- a/.aiox-core/core/synapse/engine.js +++ b/.aiox-core/core/synapse/engine.js @@ -180,6 +180,14 @@ const PIPELINE_TIMEOUT_MS = 100; const DEFAULT_ACTIVE_LAYERS = [0, 1, 2]; const LEGACY_MODE = process.env.SYNAPSE_LEGACY_MODE === 'true'; +function getLayerError(layer) { + if (!layer) return null; + if (typeof layer.getLastError === 'function') { + return layer.getLastError(); + } + return null; +} + /** * Orchestrates the 8-layer SYNAPSE context injection pipeline. * @@ -298,14 +306,25 @@ class SynapseEngine { previousLayers, }); - const result = layer._safeProcess(context); + let result; + try { + result = layer._safeProcess(context); + } catch (error) { + metrics.errorLayer(layer.name, error); + continue; + } if (result && Array.isArray(result.rules)) { metrics.endLayer(layer.name, result.rules.length); results.push(result); previousLayers.push(result); } else if (result === null || result === undefined) { - metrics.skipLayer(layer.name, 'Returned null'); + const layerError = getLayerError(layer); + if (layerError) { + metrics.errorLayer(layer.name, layerError); + } else { + metrics.skipLayer(layer.name, 'Returned null'); + } } else { metrics.skipLayer(layer.name, 'Invalid result format'); } diff --git a/.aiox-core/core/synapse/layers/layer-processor.js b/.aiox-core/core/synapse/layers/layer-processor.js index a20a177c15..b218c48835 100644 --- a/.aiox-core/core/synapse/layers/layer-processor.js +++ b/.aiox-core/core/synapse/layers/layer-processor.js @@ -34,6 +34,7 @@ class LayerProcessor { this.name = name; this.layer = layer; this.timeout = timeout; + this._lastError = null; } /** @@ -65,6 +66,7 @@ class LayerProcessor { */ _safeProcess(context) { const start = Date.now(); + this._lastError = null; try { const result = this.process(context); const elapsed = Date.now() - start; @@ -73,10 +75,20 @@ class LayerProcessor { } return result; } catch (error) { - console.warn(`[synapse:${this.name}] Error: ${error.message}`); + this._lastError = error instanceof Error ? error : new Error(String(error)); + console.warn(`[synapse:${this.name}] Error: ${this._lastError.message}`); return null; } } + + /** + * Return the last error captured by _safeProcess(), if any. + * + * @returns {Error|null} Last captured processing error. + */ + getLastError() { + return this._lastError; + } } module.exports = LayerProcessor; diff --git a/.aiox-core/install-manifest.yaml b/.aiox-core/install-manifest.yaml index 9a5aada78a..9508ed3bef 100644 --- a/.aiox-core/install-manifest.yaml +++ b/.aiox-core/install-manifest.yaml @@ -8,7 +8,7 @@ # - File types for categorization # version: 5.1.15 -generated_at: "2026-05-08T06:26:53.273Z" +generated_at: "2026-05-08T06:38:44.310Z" generator: scripts/generate-install-manifest.js file_count: 1111 files: @@ -1129,9 +1129,9 @@ files: type: core size: 8122 - path: core/synapse/engine.js - hash: sha256:47bab93a2144edce875ee68cc2717fee1bd824c9affdd5964454f5141310b8e0 + hash: sha256:3a459378da54c9c59102712e86a8800b6a854db1aee58c85fa54b45c214ffb66 type: core - size: 13602 + size: 14047 - path: core/synapse/layers/l0-constitution.js hash: sha256:2123a6a44915aaac2a6bbd26c67c285c9d1e12b50fe42a8ada668306b07d1c4a type: core @@ -1165,9 +1165,9 @@ files: type: core size: 4672 - path: core/synapse/layers/layer-processor.js - hash: sha256:15f9e4c1525d3fa2186170705a26191ad87d94ffd7fa7d61f373b07b6fb3d874 + hash: sha256:9cdb5efb2e95780373dd0ce8dcb64791dd1471128fc6914d274c6744036842a3 type: core - size: 2882 + size: 3222 - path: core/synapse/memory/memory-bridge.js hash: sha256:820875f97ceea80fc6402c0dab1706cfe58de527897b22dea68db40b0d6ec368 type: core diff --git a/tests/synapse/engine.test.js b/tests/synapse/engine.test.js index 10e70d8379..dd45495469 100644 --- a/tests/synapse/engine.test.js +++ b/tests/synapse/engine.test.js @@ -395,6 +395,35 @@ describe('SynapseEngine', () => { expect(calls[i].prevCount).toBeGreaterThanOrEqual(calls[i - 1].prevCount); } }); + + test('should record captured layer processing errors in metrics', async () => { + const layer = engine.layers[0]; + layer._safeProcess = jest.fn(() => null); + layer.getLastError = jest.fn(() => new Error('Layer processing failed')); + + const result = await engine.process('test', {}); + + expect(result.metrics.per_layer[layer.name]).toEqual(expect.objectContaining({ + status: 'error', + error: 'Layer processing failed', + })); + expect(result.metrics.layers_errored).toBeGreaterThanOrEqual(1); + }); + + test('should record direct _safeProcess exceptions in metrics', async () => { + const layer = engine.layers[0]; + layer._safeProcess = jest.fn(() => { + throw new Error('Unsafe layer crash'); + }); + + const result = await engine.process('test', {}); + + expect(result.metrics.per_layer[layer.name]).toEqual(expect.objectContaining({ + status: 'error', + error: 'Unsafe layer crash', + })); + expect(result.metrics.layers_errored).toBeGreaterThanOrEqual(1); + }); }); describe('process() — metrics', () => { diff --git a/tests/synapse/layer-processor.test.js b/tests/synapse/layer-processor.test.js index 674c921d4d..9bf4932b72 100644 --- a/tests/synapse/layer-processor.test.js +++ b/tests/synapse/layer-processor.test.js @@ -104,12 +104,43 @@ describe('LayerProcessor', () => { const result = processor._safeProcess({}); expect(result).toBeNull(); + expect(processor.getLastError()).toBeInstanceOf(Error); + expect(processor.getLastError().message).toBe('Something went wrong'); expect(warnSpy).toHaveBeenCalledWith( '[synapse:error-test] Error: Something went wrong', ); warnSpy.mockRestore(); }); + test('should clear last error after a successful retry', () => { + class FlakyProcessor extends LayerProcessor { + constructor() { + super({ name: 'flaky', layer: 0 }); + this.shouldFail = true; + } + process() { + if (this.shouldFail) { + this.shouldFail = false; + throw new Error('First attempt failed'); + } + return { rules: ['recovered'], metadata: { layer: 0 } }; + } + } + + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(); + const processor = new FlakyProcessor(); + + try { + expect(processor._safeProcess({})).toBeNull(); + expect(processor.getLastError().message).toBe('First attempt failed'); + + expect(processor._safeProcess({})).toEqual({ rules: ['recovered'], metadata: { layer: 0 } }); + expect(processor.getLastError()).toBeNull(); + } finally { + warnSpy.mockRestore(); + } + }); + test('should warn when timeout exceeded but still return result', () => { class SlowProcessor extends LayerProcessor { constructor() { From f89024695d56caed5d4806acac232e43b5e7c759 Mon Sep 17 00:00:00 2001 From: rafaelscosta Date: Fri, 8 May 2026 03:51:55 -0300 Subject: [PATCH 2/2] fix(synapse): guard layer error accessors --- .aiox-core/core/synapse/engine.js | 12 +++++++++++- .aiox-core/install-manifest.yaml | 6 +++--- tests/synapse/engine.test.js | 16 ++++++++++++++++ 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/.aiox-core/core/synapse/engine.js b/.aiox-core/core/synapse/engine.js index 6caf13f30c..6b40de180d 100644 --- a/.aiox-core/core/synapse/engine.js +++ b/.aiox-core/core/synapse/engine.js @@ -180,10 +180,20 @@ const PIPELINE_TIMEOUT_MS = 100; const DEFAULT_ACTIVE_LAYERS = [0, 1, 2]; const LEGACY_MODE = process.env.SYNAPSE_LEGACY_MODE === 'true'; +/** + * Safely read the last processing error exposed by a layer. + * + * @param {object} layer - Synapse layer instance. + * @returns {Error|null} Last layer error, or the accessor failure as an Error. + */ function getLayerError(layer) { if (!layer) return null; if (typeof layer.getLastError === 'function') { - return layer.getLastError(); + try { + return layer.getLastError(); + } catch (error) { + return error instanceof Error ? error : new Error(String(error)); + } } return null; } diff --git a/.aiox-core/install-manifest.yaml b/.aiox-core/install-manifest.yaml index 9508ed3bef..6221488749 100644 --- a/.aiox-core/install-manifest.yaml +++ b/.aiox-core/install-manifest.yaml @@ -8,7 +8,7 @@ # - File types for categorization # version: 5.1.15 -generated_at: "2026-05-08T06:38:44.310Z" +generated_at: "2026-05-08T06:49:15.800Z" generator: scripts/generate-install-manifest.js file_count: 1111 files: @@ -1129,9 +1129,9 @@ files: type: core size: 8122 - path: core/synapse/engine.js - hash: sha256:3a459378da54c9c59102712e86a8800b6a854db1aee58c85fa54b45c214ffb66 + hash: sha256:cd26f9a88527744fa4984fc006c428563d6a1ef023a9feb2dd8da073b4e70b9f type: core - size: 14047 + size: 14362 - path: core/synapse/layers/l0-constitution.js hash: sha256:2123a6a44915aaac2a6bbd26c67c285c9d1e12b50fe42a8ada668306b07d1c4a type: core diff --git a/tests/synapse/engine.test.js b/tests/synapse/engine.test.js index dd45495469..cf1f7b76d8 100644 --- a/tests/synapse/engine.test.js +++ b/tests/synapse/engine.test.js @@ -424,6 +424,22 @@ describe('SynapseEngine', () => { })); expect(result.metrics.layers_errored).toBeGreaterThanOrEqual(1); }); + + test('should record getLastError accessor failures in metrics', async () => { + const layer = engine.layers[0]; + layer._safeProcess = jest.fn(() => null); + layer.getLastError = jest.fn(() => { + throw new Error('Last error unavailable'); + }); + + const result = await engine.process('test', {}); + + expect(result.metrics.per_layer[layer.name]).toEqual(expect.objectContaining({ + status: 'error', + error: 'Last error unavailable', + })); + expect(result.metrics.layers_errored).toBeGreaterThanOrEqual(1); + }); }); describe('process() — metrics', () => {