affaan-m/ECC · error · Error

Canonical session snapshot requires aggregates.healths to be

Error message

Canonical session snapshot requires aggregates.healths to be an object

What it means

aggregates.healths is missing or not a plain object. Symmetric to states: must be a map of health string keys (healthy/degraded/stale/unknown) to non-negative integer counts. buildAggregates derives this from worker.health on each worker.

Source

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

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

function resolveRecordingDir(options = {}) {
  if (typeof options.recordingDir === 'string' && options.recordingDir.length > 0) {
    return path.resolve(options.recordingDir);

View on GitHub (pinned to 01e15490f0)

Solutions

  1. Use buildAggregates(workers); it always returns healths as an object (possibly empty {}).
  2. If building by hand for zero workers, use healths: {} not null.
  3. When adapting a source with no health signal, derive health via deriveWorkerHealth(rawWorker) before building aggregates.
  4. Pin a unit test that asserts both states and healths are objects after buildAggregates.

Example fix

// before
aggregates: { workerCount: workers.length, states: buildStates(workers), healths: null }

// after
aggregates: buildAggregates(workers) // yields { workerCount, states, healths } together
Defensive patterns

Strategy: type-guard

Validate before calling

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

Type guard

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

Try / catch

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

Prevention

When it happens

Trigger: Hand-built snapshot omits healths. Adapter version that stored only states and skipped healths. Empty-workers case where author set healths:null.

Common situations: Custom adapter that does not classify worker health yet and omits the field. Migration from a schema that only tracked states.

Related errors


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