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
- Print/log the key right before the call and confirm it is 12 lowercase hex chars; trim whitespace and quotes from shell variables.
- Re-derive the key from the current state dir (the file that `open` registered) instead of reusing a cached or hand-copied key.
- 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.
- 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
- Never hand-edit or truncate session keys; always read them from session state.
- Trim whitespace/quotes when keys travel through shell variables.
- Rerun `open` after clearing or switching state dirs so keys match the active server.
- Validate keys with the same 12-hex regex the CLI uses before any network call.
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
- all overlays must be readable local files
- all takes must be readable local files
- Arguments must not contain NUL bytes.
- asset name must be a simple filename stem (letters, digits…
- At least one guided harness must be selected
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)