affaan-m/ECC · error · Error
Canonical session snapshot requires workers[${index}] to be
Error message
Canonical session snapshot requires workers[${index}] to be an object What it means
Thrown by validateCanonicalSnapshot() while iterating snapshot.workers: an entry at position `index` failed the isObject() check (it was null, an array, or a primitive). The canonical schema (ecc.session.v1) requires every workers[] entry to be a plain object with id/label/state/health/runtime/intent/outputs/artifacts. Validation runs both on hand-supplied snapshots and as a self-check inside every normalize*Session builder before persist.
Source
Thrown at scripts/lib/session-adapters/canonical-session.js:196
ensureString(snapshot.session.id, 'session.id');
ensureString(snapshot.session.kind, 'session.kind');
ensureString(snapshot.session.state, 'session.state');
ensureOptionalString(snapshot.session.repoRoot, 'session.repoRoot');
if (!isObject(snapshot.session.sourceTarget)) {
throw new Error('Canonical session snapshot requires session.sourceTarget to be an object');
}
ensureString(snapshot.session.sourceTarget.type, 'session.sourceTarget.type');
ensureString(snapshot.session.sourceTarget.value, 'session.sourceTarget.value');
if (!Array.isArray(snapshot.workers)) {
throw new Error('Canonical session snapshot requires workers to be an array');
}
snapshot.workers.forEach((worker, index) => {
if (!isObject(worker)) {
throw new Error(`Canonical session snapshot requires workers[${index}] to be an object`);
}
ensureString(worker.id, `workers[${index}].id`);
ensureString(worker.label, `workers[${index}].label`);
ensureString(worker.state, `workers[${index}].state`);
ensureString(worker.health, `workers[${index}].health`);
ensureOptionalString(worker.branch, `workers[${index}].branch`);
ensureOptionalString(worker.worktree, `workers[${index}].worktree`);
if (!isObject(worker.runtime)) {
throw new Error(`Canonical session snapshot requires workers[${index}].runtime to be an object`);
}
ensureString(worker.runtime.kind, `workers[${index}].runtime.kind`);
ensureOptionalString(worker.runtime.command, `workers[${index}].runtime.command`);
ensureBoolean(worker.runtime.active, `workers[${index}].runtime.active`);
ensureBoolean(worker.runtime.dead, `workers[${index}].runtime.dead`);
View on GitHub (pinned to 01e15490f0)
Solutions
- Inspect snapshot.workers[index]: if null/array/primitive, either drop the entry or rebuild it as an object matching the worker shape (id,label,state,health,runtime,intent,outputs,artifacts).
- If the snapshot came from a recording file, open the JSON and confirm workers[] entries are objects; re-record from the live adapter if corrupted.
- When building a custom adapter, filter workers before validation: workers = rawWorkers.filter(w => w && typeof w === 'object').
- Reproduce with a minimal snapshot and print JSON.stringify(snapshot.workers[index], null, 2) to see exactly what was passed.
Example fix
// before
const snapshot = {
schemaVersion: 'ecc.session.v1',
adapterId: 'custom',
session: { id: 's1', kind: 'custom', state: 'active', sourceTarget: { type: 'custom', value: 's1' } },
workers: [null], // throws at index 0
aggregates: { workerCount: 1, states: {}, healths: {} }
};
// after: drop or materialize the placeholder
const workers = rawWorkers
.filter(w => w && typeof w === 'object')
.map(materializeWorker);
const snapshot = {
schemaVersion: 'ecc.session.v1',
adapterId: 'custom',
session: { id: 's1', kind: 'custom', state: 'active', sourceTarget: { type: 'custom', value: 's1' } },
workers,
aggregates: buildAggregates(workers)
}; Defensive patterns
Strategy: validation
Validate before calling
function sanitizeWorkers(workers) {
if (!Array.isArray(workers)) return [];
return workers.filter(w => w !== null && typeof w === 'object' && !Array.isArray(w));
}
// before persisting:
snapshot.workers = sanitizeWorkers(snapshot.workers);
snapshot.aggregates = buildAggregates(snapshot.workers); Type guard
function isPlainObject(v) { return v !== null && typeof v === 'object' && !Array.isArray(v); }
function isWorkerEntry(w) {
return isPlainObject(w)
&& typeof w.id === 'string' && typeof w.label === 'string'
&& isPlainObject(w.runtime) && isPlainObject(w.intent)
&& isPlainObject(w.outputs) && isPlainObject(w.artifacts);
} Try / catch
try { validateCanonicalSnapshot(snapshot); }
catch (err) {
if (err.message.includes('workers[')) {
// log the failing index from the message, sanitize, and rebuild aggregates
snapshot.workers = (snapshot.workers||[]).filter(isWorkerEntry);
snapshot.aggregates = buildAggregates(snapshot.workers);
validateCanonicalSnapshot(snapshot);
} else throw err;
} Prevention
- Always build workers via the normalize*Session helpers rather than constructing them inline.
- Treat snapshots as immutable: derive workers and aggregates in the same step.
- Add a unit test per adapter that runs validateCanonicalSnapshot on the normalize* output.
When it happens
Trigger: Calling validateCanonicalSnapshot(snapshot) or persistCanonicalSnapshot(snapshot) where snapshot.workers contains a non-object element (e.g. workers:[null] or workers:["w1"]). Reading a recorded canonical snapshot JSON from disk whose workers array was hand-edited or written by an older adapter that emitted null placeholders for dead panes.
Common situations: A future or custom adapter maps a missing dmux pane to null in the workers array instead of skipping it. A user reloads a persisted recording that was truncated/corrupted on write. Snapshot is assembled manually from partial data and a worker slot is left as a placeholder string.
Related errors
- Canonical session snapshot requires workers[${index}].runtim
- Canonical session snapshot requires workers[${index}].intent
- Canonical session snapshot requires workers[${index}].output
- Canonical session snapshot requires workers[${index}].artifa
- Canonical session snapshot requires aggregates to be an obje
AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13).
Data as JSON: /api/errors/c3eaf1c7886760d0.
Report an issue: GitHub.