affaan-m/ECC · error · Error

invalid plan-canvas session key

Error message

invalid plan-canvas session key

What it means

awaitRequest polls the canvas server's /api/await endpoint using a session key that must be exactly 12 lowercase hex characters (a 12-char server-generated session id). Before any network work, the key is validated with the regex /^[a-f0-9]{12}$/; anything else — empty, uppercase, wrong length, or containing separators — throws 'invalid plan-canvas session key' immediately, protecting the URL from malformed or injected keys.

Solutions

  1. Print/log the key right before the call and confirm it is 12 lowercase hex chars; trim whitespace and quotes from shell variables.
  2. Re-derive the key from the current state dir (the file that `open` registered) instead of reusing a cached or hand-copied key.
  3. If the state dir was cleared or migrated, rerun `ecc-plan-canvas open <file>` to mint a fresh session key, then `await` with that key.
  4. If you hardcode the key in a script, fetch it programmatically from the session state instead of a literal so version/format changes cannot break it.

Example fix

// before (stale/unknown format key)
await awaitRequest(port, process.env.SESSION_KEY, timeout);

// after (validate before calling)
const key = (process.env.SESSION_KEY || '').trim().toLowerCase();
if (!/^[a-f0-9]{12}$/.test(key)) {
  throw new Error(`bad session key "${key}" — rerun ecc-plan-canvas open to get a fresh one`);
}
await awaitRequest(port, key, timeout);
Defensive patterns

Strategy: validation

Validate before calling

if (!/^[a-f0-9]{12}$/.test(key)) throw new Error(`invalid session key: ${JSON.stringify(key)}`);

Type guard

const isSessionKey = (v) => typeof v === 'string' && /^[a-f0-9]{12}$/.test(v);

Try / catch

try {
  return await awaitRequest(port, key, timeoutMs);
} catch (err) {
  if (/invalid plan-canvas session key/.test(err.message)) {
    // re-derive key from state dir or rerun `open`
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling `ecc-plan-canvas await <file>` (via result/awaitRequest) when the derived session key is not a 12-char hex string: the key was truncated or hand-edited, the session lookup (sessionKeyFor / stored state) returned undefined or an old-format key, or a caller passed the file path itself instead of the key.

Common situations: Scripting the CLI and pasting a key from an old session after the state dir was cleared (so the lookup falls back to garbage); copying a key with surrounding whitespace or quotes into a shell variable; mixing keys between two canvas state dirs (dev vs prod); a schema change in an ECC upgrade that changed key length while old scripts cache keys.

Understand the failure class

Background: "invalid id" errors: invalid identifier format — why libraries reject IDs before lookup, and how to fix them — this error's family across 37 libraries.

Related errors


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

Appendix: source

Thrown at scripts/plan-canvas.js:246

  const res = await request(port, 'POST', '/api/sessions', {
    file: path.resolve(file),
    reopen: args.includes('--reopen')
  });
  if (res.statusCode === 409) return res.body;
  if (res.statusCode !== 200) throw new Error(res.body.error || `open failed (HTTP ${res.statusCode})`);
  const url = `http://${DEFAULT_HOST}:${port}${res.body.url}`;
  const launched = args.includes('--no-open') ? false : openBrowser(url);
  return {
    status: 'open',
    url,
    browser: launched ? 'opened' : 'not opened',
    next_step:
      'Run `ecc-plan-canvas await <file>` and leave it running; it returns when the human sends feedback, a verdict, or ends the session.'
  };
}

function awaitRequest(port, key, timeoutMs) {
  if (!/^[a-f0-9]{12}$/.test(key)) throw new Error('invalid plan-canvas session key');
  const params = new URLSearchParams({ key });
  if (timeoutMs !== null) params.set('timeoutMs', String(timeoutMs));
  return new Promise((resolve, reject) => {
    const req = http.request(
      requestOptions(port, 'GET', `/api/await?${params}`, {}),
      res => {
        let data = '';
        res.on('data', chunk => {
          data += chunk;
        });
        res.on('end', () => {
          try {
            resolve(JSON.parse(data.trim()));
          } catch {
            reject(new Error('await response was not JSON (server restarted?) - re-run await; feedback is never lost'));
          }
        });
      }

View on GitHub (pinned to 8321021c54)