can1357/oh-my-pi · error

Invalid ACP session cursor: ${cursor}

Error message

Invalid ACP session cursor: ${cursor}

What it means

#parseCursor converts the ACP pagination cursor into a non-negative integer message offset. An absent cursor means 0 (start), but a present cursor that is not a finite non-negative integer is rejected with this error. It protects history replay from garbage cursors that would silently truncate or mis-index the transcript.

Source

Thrown at packages/coding-agent/src/modes/acp/acp-agent.ts:2242

		// naming scheme. Sessions written under a legacy/hashed project directory
		// (the 17.2.5+ scheme reverted in #7656) live elsewhere, so fall back to a
		// global by-id scan: the session id is globally unique, and
		// #openStoredSession reopens the file with the request cwd. See #7779.
		return this.#findStoredSessionById(sessionId);
	}

	async #findStoredSessionById(sessionId: string): Promise<StoredSessionInfo | undefined> {
		const sessions = await this.#listStoredSessions();
		return sessions.find(session => session.id === sessionId);
	}

	#parseCursor(cursor: string | undefined): number {
		if (!cursor) {
			return 0;
		}
		const parsed = Number.parseInt(cursor, 10);
		if (!Number.isFinite(parsed) || parsed < 0) {
			throw new Error(`Invalid ACP session cursor: ${cursor}`);
		}
		return parsed;
	}

	async #replaySessionHistory(record: ManagedSessionRecord): Promise<void> {
		const cwd = record.session.sessionManager.getCwd();
		const replayedToolCallIds = new Set<string>();
		const replayedToolCallArgs = new Map<string, unknown>();
		for (const message of record.session.sessionManager.buildSessionContext().messages as ReplayableMessage[]) {
			for (const notification of this.#messageToReplayNotifications(
				record.session.sessionId,
				message,
				cwd,
				replayedToolCallIds,
				replayedToolCallArgs,
			)) {
				await this.#connection.sessionUpdate(notification);
			}

View on GitHub (pinned to 9690622007)

Solutions

  1. Send the cursor string exactly as returned by the previous page response, unmodified.
  2. Omit the cursor entirely (or send undefined) to request history from the beginning.
  3. If implementing a client, store cursors opaquely and pass them back verbatim rather than parsing them.

Example fix

// before
const history = await loadHistory({ cursor: "page-3" });
// after
const history = await loadHistory({ cursor: previousResponse.nextCursor }); // opaque token, or omit for start
Defensive patterns

Strategy: validation

Validate before calling

function isValidCursor(cursor?: string): boolean {
  if (cursor === undefined) return true;
  const n = Number.parseInt(cursor, 10);
  return Number.isFinite(n) && n >= 0 && String(n) === cursor;
}
if (!isValidCursor(cursor)) cursor = undefined; // restart from beginning

Type guard

function isCursor(v: unknown): v is string {
  return typeof v === "string" && /^\d+$/.test(v);
}

Try / catch

try {
  return await loadHistory({ cursor });
} catch (err) {
  if (err.message.startsWith("Invalid ACP session cursor")) {
    return await loadHistory({}); // restart from the beginning
  } throw err;
}

Prevention

When it happens

Trigger: Calling an ACP session/history-loading RPC with a cursor query value that is empty-but-present, non-numeric, negative, or otherwise unparseable by Number.parseInt (e.g. "abc", "-1", "1.5.2").

Common situations: A client round-trips the cursor through something that corrupts it (URL encoding, prefixing); a hand-written script guesses a cursor format; a client sends a page-token from a different API.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/f06074abb4e83310. Report an issue: GitHub.