can1357/oh-my-pi · error · Error

Agent "${resolvedAgentId}" was replaced during session initi

Error message

Agent "${resolvedAgentId}" was replaced during session initialization.

What it means

At the end of session initialization the SDK atomically attaches the session and flips the registry status to "running" using the captured registeredAgentRef. If either compare-and-swap fails, another generation replaced the agent's registry entry while construction was in progress, and the SDK refuses to continue on a stale ownership token.

Source

Thrown at packages/coding-agent/src/sdk.ts:3875

		session.yieldQueue.register<DeferredDiagnosticsEntry>(LSP_LATE_DIAGNOSTIC_MESSAGE_TYPE, {
			build: buildLateDiagnosticsBatchMessage,
			isStale: entry => entry.isStale(),
		});

		// Attach the live session to the pre-registered ref so peers can route IRC
		// messages here. Refresh sessionFile in case it was unavailable at pre-register
		// time. The dispose wrapper below unregisters on teardown (unless parked).
		if (
			!registeredAgentRef ||
			!agentRegistry.attachSession(
				resolvedAgentId,
				session,
				sessionManager.getSessionFile() ?? null,
				registeredAgentRef,
			) ||
			!agentRegistry.setStatus(resolvedAgentId, "running", registeredAgentRef)
		) {
			throw new Error(`Agent "${resolvedAgentId}" was replaced during session initialization.`);
		}
		hasRegistered = true;
		// MCP notification bridge cleanup — assigned when the bridge is wired below,
		// invoked from the dispose wrapper AND registered as a postmortem so both
		// explicit-dispose (SDK embedders that reuse the process across sessions) and
		// process-exit paths tear the listener down. Nulled after use so the closure
		// graph (`extensionRunner`, `session`) can be GC'd instead of retained by the
		// process-global postmortem list.
		let unsubscribeMcpNotifications: (() => void) | undefined;
		let unregisterMcpPostmortem: (() => void) | undefined;

		{
			const originalDispose = session.dispose.bind(session);
			session.dispose = async () => {
				try {
					// Reject new session work (eval starts) the moment disposal
					// begins — the lifecycle await below opens an async gap before
					// AgentSession.dispose() would otherwise set its guards.

View on GitHub (pinned to 9690622007)

Solutions

  1. Serialize session creation so only one process initializes a given agent at a time
  2. Retry createAgentSession after the competing operation completes
  3. Use distinct agent ids per concurrent session

Example fix

// before
await Promise.all([start(id), start(id)]);
// after
const lock = await acquireAgentLock(id);
try { await start(id); } finally { await lock.release(); }
Defensive patterns

Strategy: retry

Validate before calling

null

Type guard

null

Try / catch

try {
  session = await createAgentSession({ agentId });
} catch (err) {
  if (err instanceof Error && err.message.includes("was replaced during session initialization")) {
    session = await createAgentSession({ agentId }); // retry once after race settles
  } else throw err;
}

Prevention

When it happens

Trigger: A concurrent createAgentSession/dispose for the same resolvedAgentId changes the registry entry between the initial registration and the final setStatus("running") call.

Common situations: Two processes racing to start a session for the same agent; an automation script launching sessions in parallel; a session being disposed/restarted at the same moment a new one initializes.

Related errors


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