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
- Serialize session creation so only one process initializes a given agent at a time
- Retry createAgentSession after the competing operation completes
- 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
- Serialize session creation per agent id across processes
- Avoid disposing/restarting a session while another initialization for the same id is in flight
- Use unique agent ids for parallel sessions
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
- OAuth refresh ownership was lost before persistence
- No credential with id=${id}
- session header changed during stats cleanup: ${session.path}
- Daemon ${spec.name} is already starting
- RPC message snapshot does not match current messages
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/d355fcf42f8e47cc.
Report an issue: GitHub.