affaan-m/ECC · error · Error

Canonical session snapshot requires workers[${index}].runtim

Error message

Canonical session snapshot requires workers[${index}].runtime to be an object

What it means

A workers[index] entry passed the object check but its `runtime` field failed isObject(). The schema requires worker.runtime to be an object containing kind (string), command (string|null), active (boolean), and dead (boolean). This guard fires before the per-field ensure* checks on runtime, so any non-object runtime (undefined, null, string) is rejected.

Source

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

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

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

    ensureStringAllowEmpty(worker.intent.objective, `workers[${index}].intent.objective`);
    ensureArrayOfStrings(worker.intent.seedPaths, `workers[${index}].intent.seedPaths`);

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

View on GitHub (pinned to 01e15490f0)

Solutions

  1. Ensure each worker has runtime: { kind: '<string>', command: <string|null>, active: <boolean>, dead: <boolean> }.
  2. If adapting a dmux pane, follow normalizeDmuxSnapshot: set kind:'tmux-pane', command from pane.currentCommand||null, active/dead from pane booleans.
  3. If the source genuinely has no runtime, still emit a minimal runtime object (e.g. kind:'unknown', command:null, active:false, dead:true) rather than omitting it.
  4. Validate with a tiny unit snapshot before persisting to surface the exact missing field.

Example fix

// before
const worker = { id: 'w1', label: 'w1', state: 'active', health: 'healthy', runtime: null, intent: {...}, outputs: {...}, artifacts: {...} };

// after
const worker = {
  id: 'w1', label: 'w1', state: 'active', health: 'healthy',
  runtime: { kind: 'tmux-pane', command: pane.currentCommand || null, active: Boolean(pane.active), dead: Boolean(pane.dead) },
  intent: {...}, outputs: {...}, artifacts: {...}
};
Defensive patterns

Strategy: type-guard

Validate before calling

function ensureWorkerRuntime(worker) {
  if (!worker.runtime || typeof worker.runtime !== 'object') {
    worker.runtime = { kind: 'unknown', command: null, active: false, dead: true };
  }
  return worker;
}

Type guard

function hasValidRuntime(w) {
  const r = w.runtime;
  return r !== null && typeof r === 'object' && !Array.isArray(r)
    && typeof r.kind === 'string'
    && typeof r.active === 'boolean' && typeof r.dead === 'boolean';
}

Try / catch

try { validateCanonicalSnapshot(snapshot); }
catch (err) {
  if (err.message.includes('.runtime to be an object')) {
    snapshot.workers.forEach(w => { if (!hasValidRuntime(w)) ensureWorkerRuntime(w); });
    validateCanonicalSnapshot(snapshot);
  } else throw err;
}

Prevention

When it happens

Trigger: A custom adapter builds a worker object but forgets the runtime block, or sets runtime:null. Hand-authored snapshot JSON omits the runtime key. A migrated snapshot from a pre-runtime schema version where runtime was a string command name.

Common situations: Extending the dmux-tmux adapter to a new pane source and forgetting to populate runtime. Editing a recording JSON by hand and deleting the runtime object. Caching a worker across schema versions without re-adding runtime.

Related errors


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