earendil-works/pi · error · SessionError
already_exists
already_exists
Error message
Lane already exists: ${lane} What it means
SessionState.validateNewLane rejects createLane calls whose lane name already exists in the session's lane map. That map always starts with the default lane 'main' (state.ts:57), so re-creating 'main' or any lane previously created via createLane or a lane mutation fails. Lane names are unique per session and are never reclaimed.
Source
Thrown at packages/agent/src/harness/session/state.ts:84
private name: string | undefined;
private readonly labels = new Map<string, string>();
get nextSequence(): number {
return this.sequence + 1;
}
getLanes(): LanePointer[] {
return [...this.lanes].map(([lane, leafId]) => ({ lane, leafId }));
}
requireLane(lane: string): string | null {
const leafId = this.lanes.get(lane);
if (leafId === undefined) throw new SessionError("invalid_lane", `Lane not found: ${lane}`);
return leafId;
}
validateNewLane(lane: string): void {
if (this.lanes.has(lane)) throw new SessionError("already_exists", `Lane already exists: ${lane}`);
}
validateTarget(targetId: string | null): void {
if (targetId !== null && !this.entriesById.has(targetId)) {
throw new SessionError("not_found", `Entry not found: ${targetId}`);
}
}
validateUnusedId(id: string): void {
if (this.usedIds.has(id)) throw new SessionError("already_exists", `Session id already exists: ${id}`);
}
applyMutation(mutation: SessionMutation, invalid: InvalidMutation = invalidMutation): void {
const seq =
mutation.kind === "entry"
? mutation.entry.seq
: mutation.kind === "record"
? mutation.record.seqView on GitHub (pinned to 4af9d21d3b)
Solutions
- Pick a lane name not present in state.getLanes()
- Treat SessionError code 'already_exists' as success if you want idempotent lane creation
- Generate lane names from a uuid or counter instead of fixed strings
- Check getLanes()/requireLane() before creating so the call only happens for missing lanes
Example fix
// before
session.createLane('main', leafId); // throws already_exists
// after
if (!state.getLanes().some((p) => p.lane === 'main')) {
session.createLane('main', leafId);
} Defensive patterns
Strategy: validation
Validate before calling
// Skip creation when the lane already exists const laneExists = (state: SessionState, lane: string): boolean => state.getLanes().some((pointer) => pointer.lane === lane); if (!laneExists(state, lane)) session.createLane(lane, leafId);
Type guard
import { SessionError } from './types.ts';
function isSessionError(e: unknown, code?: string): e is SessionError {
return e instanceof SessionError && (code === undefined || e.code === code);
} Try / catch
try {
session.createLane(lane, leafId);
} catch (e) {
if (isSessionError(e, 'already_exists')) return; // idempotent create
throw e;
} Prevention
- Never create 'main'; it exists in every SessionState (state.ts:57)
- Derive lane names from uuids or a monotonic counter
- Make create paths idempotent by checking getLanes() first or swallowing already_exists
When it happens
Trigger: createLane('main') (exists by default in every SessionState); createLane with a name already returned by getLanes(); a retry wrapper re-issuing createLane after a timeout when the first attempt actually committed; two concurrent UI actions racing to create the same lane name.
Common situations: Hardcoding 'main' as a new lane name; idempotency-style retry logic that assumes the first create failed; rehydrating a session from disk and re-running the setup code that already created its lanes.
Related errors
AI-assisted analysis of earendil-works/pi@4af9d21d3b (2026-08-24).
Data as JSON: /api/errors/bfad6bca8ebf9896.
Report an issue: GitHub.