earendil-works/pi · error · SessionError

not_found

not_found

Error message

Entry not found: ${targetId}

What it means

SessionState.validateTarget throws not_found when a non-null targetId is not a known entry id (entriesById lookup, state.ts:88). It guards createLane, moveLane and setLabel, so every lane target and label target must be an existing entry in the same SessionState. Record ids (tool runs, usage) and lane names are not entry ids and are rejected.

Source

Thrown at packages/agent/src/harness/session/state.ts:89

	}

	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.seq
					: mutation.seq;
		if (seq !== this.sequence + 1) invalid(`has non-consecutive seq ${seq}`);

		switch (mutation.kind) {
			case "entry": {

View on GitHub (pinned to 4af9d21d3b)

Solutions

  1. Resolve target ids from state.findEntries() or getEntry() on the same instance
  2. Use lane leaf ids from getLanes()/requireLane() as move targets
  3. When sourcing ids from getLog(), use only items with kind === 'entry' (item.entry.id)
  4. Catch 'not_found' and re-fetch current leaf ids before retrying once

Example fix

// before
const run = state.findOpenOperations('main')[0]!;
session.moveLane('draft', run.id); // record id -> not_found

// after
const last = state.findEntries({ type: 'message' }).at(-1)!;
session.moveLane('draft', last.id);
Defensive patterns

Strategy: validation

Validate before calling

// Verify the target is an existing entry before using it
if (targetId !== null && state.getEntry(targetId) === undefined) {
  throw new Error('stale target id: ' + targetId);
}
session.moveLane(lane, targetId);

Type guard

function isEntryId(state: SessionState, id: string): boolean {
  return state.getEntry(id) !== undefined;
}

Try / catch

try {
  session.setLabel(targetId, label);
} catch (e) {
  if (isSessionError(e, 'not_found')) {
    const leaf = state.requireLane(lane); // refresh and retry once with a known-good id
    if (leaf !== null) session.setLabel(leaf, label);
  } else throw e;
}

Prevention

When it happens

Trigger: moveLane(lane, entryId) where entryId came from findRecords()/findOpenOperations() (a LaneRecord id, not an Entry id); setLabel on an entry id captured from a different session instance; createLane pointing at a leafId taken before the session was rebuilt or forked.

Common situations: Confusing record ids with entry ids because getLog() interleaves both kinds; copying ids from a persisted session file into a fresh SessionState; using stale ids after an external prune or compaction removed entries.

Understand the failure class

Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.

Related errors


AI-assisted analysis of earendil-works/pi@4af9d21d3b (2026-08-24). Data as JSON: /api/errors/7e93ca21a7e0e00b. Report an issue: GitHub.