affaan-m/ECC · error · Error

Canonical session snapshot requires aggregates.workerCount…

Error message

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

What it means

This consistency error fires when snapshot.aggregates.workerCount is an integer but does not equal snapshot.workers.length. The canonical schema treats aggregates.workerCount as a denormalized count of the workers array; the validator rejects any snapshot where the summary and the actual data disagree, since that signals a corrupted or partially updated snapshot.

Solutions

  1. Recompute aggregates.workerCount = snapshot.workers.length (and rebuild states/healths counts) immediately before persisting.
  2. Never mutate a loaded snapshot's workers in place; rebuild the whole snapshot object including aggregates.
  3. Add a helper that derives aggregates from workers so they can never drift.
  4. Run validateCanonicalSnapshot after every snapshot mutation in tests.

Example fix

// before
snapshot.workers.push(newWorker);
persistCanonicalSnapshot(snapshot); // workerCount stale
// after
snapshot.workers.push(newWorker);
snapshot = { ...snapshot, aggregates: { ...snapshot.aggregates, workerCount: snapshot.workers.length } };
persistCanonicalSnapshot(snapshot);
Defensive patterns

Strategy: validation

Validate before calling

if (snapshot.aggregates && snapshot.aggregates.workerCount !== snapshot.workers.length) throw new Error('aggregates.workerCount out of sync with workers.length');

Type guard

function aggregatesInSync(s) { return Number.isInteger(s.aggregates?.workerCount) && s.aggregates.workerCount === s.workers.length; }

Try / catch

try { persistCanonicalSnapshot(snapshot); } catch (err) { if (String(err.message).includes('workerCount to match workers.length')) { console.error('Aggregates stale:', err.message); snapshot = recomputeAggregates(snapshot); persistCanonicalSnapshot(snapshot); } else { throw err; } }

Prevention

When it happens

Trigger: Calling persistCanonicalSnapshot after mutating the workers array (push/filter/splice) without recomputing aggregates.workerCount; merging snapshots by concatenating workers while copying stale aggregates.

Common situations: Script that adds a worker to a loaded snapshot and re-persists it without updating counts; a filter that removes dead workers but keeps the old aggregate; concurrent writers where one updated workers and another updated aggregates.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/bd8ca5ffacdffa14. Report an issue: GitHub.

Appendix: 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 8321021c54)