Skip to content

Commit 19a012f

Browse files
authored
fix(synapse): surface layer processing errors (#699)
* fix(synapse): surface layer processing errors * fix(synapse): guard layer error accessors
1 parent 9496767 commit 19a012f

5 files changed

Lines changed: 125 additions & 8 deletions

File tree

.aiox-core/core/synapse/engine.js

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,24 @@ const PIPELINE_TIMEOUT_MS = 100;
180180
const DEFAULT_ACTIVE_LAYERS = [0, 1, 2];
181181
const LEGACY_MODE = process.env.SYNAPSE_LEGACY_MODE === 'true';
182182

183+
/**
184+
* Safely read the last processing error exposed by a layer.
185+
*
186+
* @param {object} layer - Synapse layer instance.
187+
* @returns {Error|null} Last layer error, or the accessor failure as an Error.
188+
*/
189+
function getLayerError(layer) {
190+
if (!layer) return null;
191+
if (typeof layer.getLastError === 'function') {
192+
try {
193+
return layer.getLastError();
194+
} catch (error) {
195+
return error instanceof Error ? error : new Error(String(error));
196+
}
197+
}
198+
return null;
199+
}
200+
183201
/**
184202
* Orchestrates the 8-layer SYNAPSE context injection pipeline.
185203
*
@@ -298,14 +316,25 @@ class SynapseEngine {
298316
previousLayers,
299317
});
300318

301-
const result = layer._safeProcess(context);
319+
let result;
320+
try {
321+
result = layer._safeProcess(context);
322+
} catch (error) {
323+
metrics.errorLayer(layer.name, error);
324+
continue;
325+
}
302326

303327
if (result && Array.isArray(result.rules)) {
304328
metrics.endLayer(layer.name, result.rules.length);
305329
results.push(result);
306330
previousLayers.push(result);
307331
} else if (result === null || result === undefined) {
308-
metrics.skipLayer(layer.name, 'Returned null');
332+
const layerError = getLayerError(layer);
333+
if (layerError) {
334+
metrics.errorLayer(layer.name, layerError);
335+
} else {
336+
metrics.skipLayer(layer.name, 'Returned null');
337+
}
309338
} else {
310339
metrics.skipLayer(layer.name, 'Invalid result format');
311340
}

.aiox-core/core/synapse/layers/layer-processor.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class LayerProcessor {
3434
this.name = name;
3535
this.layer = layer;
3636
this.timeout = timeout;
37+
this._lastError = null;
3738
}
3839

3940
/**
@@ -65,6 +66,7 @@ class LayerProcessor {
6566
*/
6667
_safeProcess(context) {
6768
const start = Date.now();
69+
this._lastError = null;
6870
try {
6971
const result = this.process(context);
7072
const elapsed = Date.now() - start;
@@ -73,10 +75,20 @@ class LayerProcessor {
7375
}
7476
return result;
7577
} catch (error) {
76-
console.warn(`[synapse:${this.name}] Error: ${error.message}`);
78+
this._lastError = error instanceof Error ? error : new Error(String(error));
79+
console.warn(`[synapse:${this.name}] Error: ${this._lastError.message}`);
7780
return null;
7881
}
7982
}
83+
84+
/**
85+
* Return the last error captured by _safeProcess(), if any.
86+
*
87+
* @returns {Error|null} Last captured processing error.
88+
*/
89+
getLastError() {
90+
return this._lastError;
91+
}
8092
}
8193

8294
module.exports = LayerProcessor;

.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.1.15
11-
generated_at: "2026-05-08T06:26:53.273Z"
11+
generated_at: "2026-05-08T06:49:15.800Z"
1212
generator: scripts/generate-install-manifest.js
1313
file_count: 1111
1414
files:
@@ -1129,9 +1129,9 @@ files:
11291129
type: core
11301130
size: 8122
11311131
- path: core/synapse/engine.js
1132-
hash: sha256:47bab93a2144edce875ee68cc2717fee1bd824c9affdd5964454f5141310b8e0
1132+
hash: sha256:cd26f9a88527744fa4984fc006c428563d6a1ef023a9feb2dd8da073b4e70b9f
11331133
type: core
1134-
size: 13602
1134+
size: 14362
11351135
- path: core/synapse/layers/l0-constitution.js
11361136
hash: sha256:2123a6a44915aaac2a6bbd26c67c285c9d1e12b50fe42a8ada668306b07d1c4a
11371137
type: core
@@ -1165,9 +1165,9 @@ files:
11651165
type: core
11661166
size: 4672
11671167
- path: core/synapse/layers/layer-processor.js
1168-
hash: sha256:15f9e4c1525d3fa2186170705a26191ad87d94ffd7fa7d61f373b07b6fb3d874
1168+
hash: sha256:9cdb5efb2e95780373dd0ce8dcb64791dd1471128fc6914d274c6744036842a3
11691169
type: core
1170-
size: 2882
1170+
size: 3222
11711171
- path: core/synapse/memory/memory-bridge.js
11721172
hash: sha256:820875f97ceea80fc6402c0dab1706cfe58de527897b22dea68db40b0d6ec368
11731173
type: core

tests/synapse/engine.test.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,51 @@ describe('SynapseEngine', () => {
395395
expect(calls[i].prevCount).toBeGreaterThanOrEqual(calls[i - 1].prevCount);
396396
}
397397
});
398+
399+
test('should record captured layer processing errors in metrics', async () => {
400+
const layer = engine.layers[0];
401+
layer._safeProcess = jest.fn(() => null);
402+
layer.getLastError = jest.fn(() => new Error('Layer processing failed'));
403+
404+
const result = await engine.process('test', {});
405+
406+
expect(result.metrics.per_layer[layer.name]).toEqual(expect.objectContaining({
407+
status: 'error',
408+
error: 'Layer processing failed',
409+
}));
410+
expect(result.metrics.layers_errored).toBeGreaterThanOrEqual(1);
411+
});
412+
413+
test('should record direct _safeProcess exceptions in metrics', async () => {
414+
const layer = engine.layers[0];
415+
layer._safeProcess = jest.fn(() => {
416+
throw new Error('Unsafe layer crash');
417+
});
418+
419+
const result = await engine.process('test', {});
420+
421+
expect(result.metrics.per_layer[layer.name]).toEqual(expect.objectContaining({
422+
status: 'error',
423+
error: 'Unsafe layer crash',
424+
}));
425+
expect(result.metrics.layers_errored).toBeGreaterThanOrEqual(1);
426+
});
427+
428+
test('should record getLastError accessor failures in metrics', async () => {
429+
const layer = engine.layers[0];
430+
layer._safeProcess = jest.fn(() => null);
431+
layer.getLastError = jest.fn(() => {
432+
throw new Error('Last error unavailable');
433+
});
434+
435+
const result = await engine.process('test', {});
436+
437+
expect(result.metrics.per_layer[layer.name]).toEqual(expect.objectContaining({
438+
status: 'error',
439+
error: 'Last error unavailable',
440+
}));
441+
expect(result.metrics.layers_errored).toBeGreaterThanOrEqual(1);
442+
});
398443
});
399444

400445
describe('process() — metrics', () => {

tests/synapse/layer-processor.test.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,43 @@ describe('LayerProcessor', () => {
104104
const result = processor._safeProcess({});
105105

106106
expect(result).toBeNull();
107+
expect(processor.getLastError()).toBeInstanceOf(Error);
108+
expect(processor.getLastError().message).toBe('Something went wrong');
107109
expect(warnSpy).toHaveBeenCalledWith(
108110
'[synapse:error-test] Error: Something went wrong',
109111
);
110112
warnSpy.mockRestore();
111113
});
112114

115+
test('should clear last error after a successful retry', () => {
116+
class FlakyProcessor extends LayerProcessor {
117+
constructor() {
118+
super({ name: 'flaky', layer: 0 });
119+
this.shouldFail = true;
120+
}
121+
process() {
122+
if (this.shouldFail) {
123+
this.shouldFail = false;
124+
throw new Error('First attempt failed');
125+
}
126+
return { rules: ['recovered'], metadata: { layer: 0 } };
127+
}
128+
}
129+
130+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
131+
const processor = new FlakyProcessor();
132+
133+
try {
134+
expect(processor._safeProcess({})).toBeNull();
135+
expect(processor.getLastError().message).toBe('First attempt failed');
136+
137+
expect(processor._safeProcess({})).toEqual({ rules: ['recovered'], metadata: { layer: 0 } });
138+
expect(processor.getLastError()).toBeNull();
139+
} finally {
140+
warnSpy.mockRestore();
141+
}
142+
});
143+
113144
test('should warn when timeout exceeded but still return result', () => {
114145
class SlowProcessor extends LayerProcessor {
115146
constructor() {

0 commit comments

Comments
 (0)