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
33 changes: 31 additions & 2 deletions .aiox-core/core/synapse/engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,24 @@ 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') {
try {
return layer.getLastError();
} catch (error) {
return error instanceof Error ? error : new Error(String(error));
}
}
return null;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* Orchestrates the 8-layer SYNAPSE context injection pipeline.
*
Expand Down Expand Up @@ -298,14 +316,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');
}
Expand Down
14 changes: 13 additions & 1 deletion .aiox-core/core/synapse/layers/layer-processor.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class LayerProcessor {
this.name = name;
this.layer = layer;
this.timeout = timeout;
this._lastError = null;
}

/**
Expand Down Expand Up @@ -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;
Expand All @@ -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;
10 changes: 5 additions & 5 deletions .aiox-core/install-manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:49:15.800Z"
generator: scripts/generate-install-manifest.js
file_count: 1111
files:
Expand Down Expand Up @@ -1129,9 +1129,9 @@ files:
type: core
size: 8122
- path: core/synapse/engine.js
hash: sha256:47bab93a2144edce875ee68cc2717fee1bd824c9affdd5964454f5141310b8e0
hash: sha256:cd26f9a88527744fa4984fc006c428563d6a1ef023a9feb2dd8da073b4e70b9f
type: core
size: 13602
size: 14362
- path: core/synapse/layers/l0-constitution.js
hash: sha256:2123a6a44915aaac2a6bbd26c67c285c9d1e12b50fe42a8ada668306b07d1c4a
type: core
Expand Down Expand Up @@ -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
Expand Down
45 changes: 45 additions & 0 deletions tests/synapse/engine.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,51 @@ 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);
});

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', () => {
Expand Down
31 changes: 31 additions & 0 deletions tests/synapse/layer-processor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Loading