can1357/oh-my-pi · error · ToolError

Vibe session "${record.id}" changed parent scope before term

Error message

Vibe session "${record.id}" changed parent scope before termination.

What it means

For each pending worker record with a childSessionFile, #persistModeExit recomputes the expected child session path from the CURRENT parent session file and compares it with the path recorded at spawn time. A mismatch means the parent session file moved/changed after the worker was spawned, so tombstones would land in the wrong place; it throws naming the offending record id.

Source

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

			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
			) {
				throw new ToolError(`Vibe session "${record.id}" changed parent scope before termination.`);
			}
		}
		const appendEntriesAtomically = sessionManager.appendEntriesAtomically;
		if (!appendEntriesAtomically) {
			throw new ToolError("Vibe mode exit requires atomic parent-session persistence.");
		}
		await appendEntriesAtomically.call(sessionManager, () => {
			for (const record of persistedPending) {
				sessionManager.appendCustomEntry(VIBE_LIFECYCLE_CUSTOM_TYPE, {
					...this.#eventBase(record),
					action: "tombstone",
					reason: "mode-exit",
				});
			}
			sessionManager.appendModeChange?.("none");
		});
		for (const record of pending) record.terminalPersisted = true;
	}

View on GitHub (pinned to 9690622007)

Solutions

  1. Keep the parent session file path stable while vibe workers are alive; if the file must move, exit vibe mode first.
  2. Purge or re-create worker records so childSessionFile matches the current parent file location before exiting.
  3. Check for double-resolution bugs: record.childSessionFile is expected to already be absolute and derived from the same parent file.

Example fix

// before
renameSessionFile(session, newPath); // workers spawned under old path
await vibe.exit(session); // throws for record id
// after
await vibe.exit(session); // persist tombstones first
renameSessionFile(session, newPath);
Defensive patterns

Strategy: try-catch

Validate before calling

const parentFile = session.getSessionFile();
for (const rec of activeRecords) {
  if (rec.childSessionFile && path.resolve(path.dirname(parentFile!), `${rec.id}.jsonl`) !== rec.childSessionFile) {
    throw new Error(`Record ${rec.id} is parented to a different session file; exit vibe mode before moving the session`);
  }
}

Try / catch

try {
  await vibe.exit(session);
} catch (err) {
  if (err instanceof ToolError && /changed parent scope before termination/.test(err.message)) {
    const id = err.message.match(/"([^"]+)"/)?.[1];
    logger.warn("vibe worker parented to stale session file", { id });
  } else throw err;
}

Prevention

When it happens

Trigger: vibe exit (→ #killAllLocked → #persistModeExit) where path.resolve(parentSessionFile minus .jsonl suffix, `${record.id}.jsonl`) differs from record.childSessionFile — typically because the parent session file changed between spawn and exit, or the record was built under a different session file.

Common situations: Session file relocated or renamed (resume-from-copy) while workers were alive; worker records rebuilt from persisted events under a new session file path; mixing records across forked parent sessions.

Related errors


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