Yeachan-Heo/oh-my-codex · error · SessionPointerLaunchAbort
session_pointer_unusable
session_pointer_unusable
Error message
Selected session pointer is ${pointer.status} and is preserved. What it means
Thrown when, for an already-authorized binding, the freshly read pointer is either not 'usable' (and not an unobservable-live 'identity-indeterminate') or fails isAuthorizedBoundPointer (the pointer must belong to this binding: canonical session id + launch lineage token match). The existing pointer is deliberately preserved and the transaction aborts via unusablePointerAbort with code session_pointer_unusable.
Source
Thrown at src/hooks/session.ts:3047
let pointerBeforeTransaction: SessionPointerReadResult;
try {
pointerBeforeTransaction = await readSessionPointer(binding.context);
} catch (error) {
throw resolvedAbort(binding.context, {
code: 'session_pointer_io_failure', operation: 'pointer-read', candidateSessionId: binding.canonicalSessionId,
lockPath: binding.context.lockPath, reason: `Unable to read selected session pointer: ${errorMessage(error)}`, cause: error,
});
}
// Read against an ALREADY-authorized binding: isAuthorizedBoundPointer verifies the pointer
// belongs to this binding (canonical session id + launch lineage token), which is a stricter
// check than liveness. Admitting an unobservable-live pointer here only stops a running session
// on a platform without process-birth evidence from reading as absent; it grants no new
// authority, because the binding authorization still has to match.
if ((pointerBeforeTransaction.status !== 'usable'
&& !(pointerBeforeTransaction.status === 'identity-indeterminate'
&& isUnobservableLivePointerState(pointerBeforeTransaction.state)))
|| !isAuthorizedBoundPointer(binding, binding.context, binding.canonicalSessionId, pointerBeforeTransaction.state)) {
throw unusablePointerAbort(binding.context, binding.canonicalSessionId, pointerBeforeTransaction);
}
const authorization = await authorizeBoundDirectoryBeforeTransaction(
binding,
pointerBeforeTransaction,
binding.context.cwd,
);
capability.push(...authorization.capability);
const result = await writePointerTransaction(
binding.context.cwd,
binding.canonicalSessionId,
{ context: binding.context },
DEFAULT_POINTER_TIMEOUT_MS,
async (pointer, context) => {
const state = pointer.status === 'usable'
|| (pointer.status === 'identity-indeterminate' && isUnobservableLivePointerState(pointer.state))
? pointer.state
: undefined;View on GitHub (pinned to 3ad79a8a6f)
Solutions
- Start a fresh session instead of resuming, or re-derive the binding from the current pointer state.
- Verify you are passing the exact session ID returned by the begin/create call for this lineage.
- Inspect the pointer file to see which session_id/lineage token it records and reconcile.
- Ensure only one process mutates a given session's pointer at a time.
Defensive patterns
Strategy: try-catch
Validate before calling
const stored = JSON.parse(await fsp.readFile(pointerPath, 'utf8'));
if (normalize(stored.session_id) !== normalize(binding.canonicalSessionId)) throw new Error('binding does not own pointer'); Type guard
const ownsPointer = (b: Binding, s: SessionState): boolean => normalizeSessionId(s.session_id) === b.canonicalSessionId;
Try / catch
try { await op(binding); } catch (e) { if (errCode(e) === 'session_pointer_unusable') return beginFreshSession(); throw e; } Prevention
- Always pass the session ID returned by begin/create — no retyping.
- One process per session pointer at a time.
- Re-create bindings after the underlying session restarts.
When it happens
Trigger: Resuming a session whose pointer state on disk belongs to a different session ID or lineage (e.g. the pointer was replaced by another concurrent session), or the pointer is stale-dead/malformed while the caller expected a live authorized one.
Common situations: Two processes resuming the 'same' session from different launch lineages (tmux respawn vs new attach); the state dir was reinitialized while an old binding was reused; a session ID typo causing an ID mismatch with the stored pointer.
Related errors
- Refusing mutating ultragoal command "${command}" from Team w
- status must be one of running, blocked, failed, complete
- autopilot.session_id must match the selected writable sessio
- Refusing a durable ultragoal mutation before writable lifecy
- Refusing durable ultragoal mutation after writable lifecycle
AI-assisted analysis of Yeachan-Heo/oh-my-codex@3ad79a8a6f (2026-08-27).
Data as JSON: /api/errors/2b5a26b919c72e2a.
Report an issue: GitHub.