can1357/oh-my-pi · error · ToolError

Vibe mode exit cannot persist worker tombstones without the

Error message

Vibe mode exit cannot persist worker tombstones without the parent session manager.

What it means

When vibe mode exits, worker tombstones must be appended to the parent session so records survive restarts. #persistModeExit throws if any pending record has a childSessionFile but session.sessionManager is null — persistence is impossible. Records without a child session file are silently marked terminal-persisted instead.

Source

Thrown at packages/coding-agent/src/vibe/runtime.ts:490

			) {
				continue;
			}
			if (event.action === "tombstone") terminalReason = event.reason;
			else if (event.action === "tombstone-revoked" && terminalReason === "mode-exit") terminalReason = undefined;
		}
		return terminalReason !== undefined;
	}

	async #persistModeExit(
		session: VibeParentSession,
		scope: VibeOwnerScope,
		records: readonly VibeRecord[],
	): Promise<void> {
		const pending = records.filter(record => !record.terminalPersisted);
		const sessionManager = session.sessionManager;
		if (!sessionManager) {
			if (pending.some(record => record.childSessionFile)) {
				throw new ToolError("Vibe mode exit cannot persist worker tombstones without the parent session manager.");
			}
			for (const record of pending) record.terminalPersisted = true;
			return;
		}
		const currentScope = this.ownerScope(session);
		if (
			currentScope.ownerId !== scope.ownerId ||
			currentScope.parentSessionId !== scope.parentSessionId ||
			currentScope.parentSessionFile !== scope.parentSessionFile
		) {
			throw new ToolError("Vibe parent session changed before mode exit could be persisted.");
		}
		const parentSessionFile = currentScope.parentSessionFile;
		const persistedPending = pending.filter(record => record.childSessionFile !== undefined);
		for (const record of persistedPending) {
			if (
				!parentSessionFile ||
				path.resolve(parentSessionFile.slice(0, -6), `${record.id}.jsonl`) !== record.childSessionFile

View on GitHub (pinned to 9690622007)

Solutions

  1. Attach a real session manager to the parent session (session.sessionManager) before exiting vibe mode.
  2. If persistence is intentionally impossible, exit vibe mode before any worker with a child session file exists, or clear the childSessionFile association.
  3. For ephemeral use, ensure records have no childSessionFile so the runtime can mark them terminal without persistence.

Example fix

// before
const session = createStubSession(); // sessionManager undefined
await vibe.exit(session); // throws when workers exist
// after
const session = createStubSession({ sessionManager: new SessionManager(sessionFile) });
await vibe.exit(session);
Defensive patterns

Strategy: validation

Validate before calling

if (!session.sessionManager) {
  throw new Error("Refusing vibe exit: session manager required to persist worker tombstones");
}

Type guard

function canPersistTombstones(s: VibeParentSession): boolean {
  return !!s.sessionManager;
}

Try / catch

try {
  await vibe.exit(session);
} catch (err) {
  if (err instanceof ToolError && err.message.includes("without the parent session manager")) {
    logger.warn("vibe exit skipped tombstone persistence; attach a session manager", { sessionId: session.getSessionId?.() });
  } else throw err;
}

Prevention

When it happens

Trigger: Calling vibe exit/kill-all (→ #killAllLocked → #persistModeExit) on a parent session whose sessionManager is unset, while at least one worker record still carries a childSessionFile that needs a tombstone appended.

Common situations: SDK embeddings that build a VibeParentSession without wiring a session manager; ephemeral/in-memory sessions used in tests; sessions opened in a mode that disables persistence (e.g. no session file configured).

Related errors


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