affaan-m/ECC · error · Error

Canonical session snapshot requires aggregates.workerCount t

Error message

Canonical session snapshot requires aggregates.workerCount to match workers.length

What it means

aggregates.workerCount is a valid non-negative integer but does not equal snapshot.workers.length. This is a consistency check: aggregates must reflect the same worker set as workers[]. The built-in buildAggregates sets workerCount = workers.length, so a mismatch means aggregates were computed against a different (stale or hand-edited) worker list.

Source

Thrown at scripts/lib/session-adapters/canonical-session.js:241

      throw new Error(`Canonical session snapshot requires workers[${index}].outputs to be an object`);
    }

    ensureArrayOfStrings(worker.outputs.summary, `workers[${index}].outputs.summary`);
    ensureArrayOfStrings(worker.outputs.validation, `workers[${index}].outputs.validation`);
    ensureArrayOfStrings(worker.outputs.remainingRisks, `workers[${index}].outputs.remainingRisks`);

    if (!isObject(worker.artifacts)) {
      throw new Error(`Canonical session snapshot requires workers[${index}].artifacts to be an object`);
    }
  });

  if (!isObject(snapshot.aggregates)) {
    throw new Error('Canonical session snapshot requires aggregates to be an object');
  }

  ensureInteger(snapshot.aggregates.workerCount, 'aggregates.workerCount');
  if (snapshot.aggregates.workerCount !== snapshot.workers.length) {
    throw new Error('Canonical session snapshot requires aggregates.workerCount to match workers.length');
  }

  if (!isObject(snapshot.aggregates.states)) {
    throw new Error('Canonical session snapshot requires aggregates.states to be an object');
  }

  if (!isObject(snapshot.aggregates.healths)) {
    throw new Error('Canonical session snapshot requires aggregates.healths to be an object');
  }

  for (const [state, count] of Object.entries(snapshot.aggregates.states)) {
    ensureString(state, 'aggregates.states key');
    ensureInteger(count, `aggregates.states.${state}`);
  }

  for (const [health, count] of Object.entries(snapshot.aggregates.healths)) {
    ensureString(health, 'aggregates.healths key');
    ensureInteger(count, `aggregates.healths.${health}`);

View on GitHub (pinned to 01e15490f0)

Solutions

  1. Recompute aggregates immediately before validation: snapshot.aggregates = buildAggregates(snapshot.workers).
  2. Never mutate workers after buildAggregates; treat snapshots as immutable and rebuild aggregates on any change.
  3. If filtering workers, rebuild aggregates from the filtered array in the same step.
  4. Add an assertion in adapter tests: expect(snapshot.aggregates.workerCount).toBe(snapshot.workers.length).

Example fix

// before
const workers = rawWorkers.filter(w => w && typeof w === 'object');
const snapshot = { schemaVersion:'ecc.session.v1', adapterId:'custom', session:{...}, workers, aggregates: cachedAggregates };

// after: recompute after final worker list is settled
const workers = rawWorkers.filter(w => w && typeof w === 'object');
const snapshot = {
  schemaVersion:'ecc.session.v1', adapterId:'custom', session:{...}, workers,
  aggregates: buildAggregates(workers)
};
Defensive patterns

Strategy: validation

Validate before calling

function reconcileAggregates(snapshot) {
  if (!snapshot.aggregates || typeof snapshot.aggregates !== 'object') {
    snapshot.aggregates = buildAggregates(snapshot.workers || []);
  } else {
    snapshot.aggregates.workerCount = (snapshot.workers || []).length;
  }
  return snapshot;
}

Type guard

function workerCountMatches(s) {
  return s.aggregates && Number.isInteger(s.aggregates.workerCount)
    && s.aggregates.workerCount === (s.workers || []).length;
}

Try / catch

try { validateCanonicalSnapshot(snapshot); }
catch (err) {
  if (err.message.includes('workerCount to match')) {
    snapshot.aggregates = buildAggregates(snapshot.workers || []);
    validateCanonicalSnapshot(snapshot);
  } else throw err;
}

Prevention

When it happens

Trigger: Workers were added/removed after aggregates were computed (e.g. aggregates built from a cached list, then workers re-filtered). Hand-editing a recording JSON to add a worker without updating workerCount. Concurrent mutation of the workers array between buildAggregates and validateCanonicalSnapshot.

Common situations: Race in an adapter that builds aggregates early then filters dead workers. Copy-paste fixture where workers array was trimmed but workerCount was not. Migration script that transforms workers but copies aggregates verbatim.

Related errors


AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13). Data as JSON: /api/errors/bd8ca5ffacdffa14. Report an issue: GitHub.