affaan-m/ECC · error · Error

Canonical session snapshot requires aggregates.states to be

Error message

Canonical session snapshot requires aggregates.states to be an object

What it means

aggregates.states is missing or not a plain object. The schema requires states to be a map of worker-state string keys to non-negative integer counts (e.g. { active: 2, completed: 1 }). buildAggregates produces this from workers; the guard fires on hand-built or stale snapshots where states is null/undefined/array.

Source

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

    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}`);
  }

  return snapshot;
}

View on GitHub (pinned to 01e15490f0)

Solutions

  1. Use buildAggregates(workers); it always returns states as an object (possibly empty {}).
  2. If building by hand for zero workers, use states: {} not null.
  3. When loading legacy recordings, coerce arrays of {state,count} into a map before validation.
  4. Re-run validateCanonicalSnapshot after any aggregate change.

Example fix

// before
aggregates: { workerCount: 0, states: null, healths: {} }

// after
aggregates: buildAggregates(workers) // yields e.g. { workerCount: 0, states: {}, healths: {} }
Defensive patterns

Strategy: type-guard

Validate before calling

function ensureStatesMap(snapshot) {
  const a = snapshot.aggregates || (snapshot.aggregates = {});
  if (a.states === null || typeof a.states !== 'object' || Array.isArray(a.states)) {
    a.states = (snapshot.workers || []).reduce((m, w) => { const s = w.state || 'unknown'; m[s] = (m[s]||0)+1; return m; }, {});
  }
  return snapshot;
}

Type guard

function hasValidStatesMap(s) {
  const st = s.aggregates && s.aggregates.states;
  return st !== null && typeof st === 'object' && !Array.isArray(st)
    && Object.entries(st).every(([k,v]) => typeof k === 'string' && Number.isInteger(v) && v >= 0);
}

Try / catch

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

Prevention

When it happens

Trigger: Hand-built snapshot omits states. Recording written by an adapter version that used an array of state objects instead of a map. aggregates.states set to null because no workers were active.

Common situations: Custom adapter constructs aggregates manually and forgets states. Empty-workers case where the author set states:null instead of states:{}.

Related errors


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