{"record":{"id":"9b5fe52c826b056b","repo":"can1357/oh-my-pi","slug":"vibe-parent-session-changed-before-mode-exit-could","errorCode":null,"errorMessage":"Vibe parent session changed before mode exit could be persisted.","messagePattern":"Vibe parent session changed before mode exit could be persisted\\.","errorType":"exception","errorClass":"ToolError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/vibe/runtime.ts","lineNumber":501,"sourceCode":"\t\tscope: VibeOwnerScope,\n\t\trecords: readonly VibeRecord[],\n\t): Promise<void> {\n\t\tconst pending = records.filter(record => !record.terminalPersisted);\n\t\tconst sessionManager = session.sessionManager;\n\t\tif (!sessionManager) {\n\t\t\tif (pending.some(record => record.childSessionFile)) {\n\t\t\t\tthrow new ToolError(\"Vibe mode exit cannot persist worker tombstones without the parent session manager.\");\n\t\t\t}\n\t\t\tfor (const record of pending) record.terminalPersisted = true;\n\t\t\treturn;\n\t\t}\n\t\tconst currentScope = this.ownerScope(session);\n\t\tif (\n\t\t\tcurrentScope.ownerId !== scope.ownerId ||\n\t\t\tcurrentScope.parentSessionId !== scope.parentSessionId ||\n\t\t\tcurrentScope.parentSessionFile !== scope.parentSessionFile\n\t\t) {\n\t\t\tthrow new ToolError(\"Vibe parent session changed before mode exit could be persisted.\");\n\t\t}\n\t\tconst parentSessionFile = currentScope.parentSessionFile;\n\t\tconst persistedPending = pending.filter(record => record.childSessionFile !== undefined);\n\t\tfor (const record of persistedPending) {\n\t\t\tif (\n\t\t\t\t!parentSessionFile ||\n\t\t\t\tpath.resolve(parentSessionFile.slice(0, -6), `${record.id}.jsonl`) !== record.childSessionFile\n\t\t\t) {\n\t\t\t\tthrow new ToolError(`Vibe session \"${record.id}\" changed parent scope before termination.`);\n\t\t\t}\n\t\t}\n\t\tconst appendEntriesAtomically = sessionManager.appendEntriesAtomically;\n\t\tif (!appendEntriesAtomically) {\n\t\t\tthrow new ToolError(\"Vibe mode exit requires atomic parent-session persistence.\");\n\t\t}\n\t\tawait appendEntriesAtomically.call(sessionManager, () => {\n\t\t\tfor (const record of persistedPending) {\n\t\t\t\tsessionManager.appendCustomEntry(VIBE_LIFECYCLE_CUSTOM_TYPE, {","sourceCodeStart":483,"sourceCodeEnd":519,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/vibe/runtime.ts#L483-L519","documentation":"#persistModeExit re-computes the owner scope at exit time and compares it to the scope captured earlier (ownerId, parentSessionId, parentSessionFile). If any differ, the parent session identity changed between vibe-mode entry and exit, and tombstones must not be written to a session that may no longer be the owner. It throws to prevent cross-session corruption.","triggerScenarios":"Calling vibe exit (→ #killAllLocked → #persistModeExit) after the parent session's id, file path, or agent id changed — e.g. the session was forked/resumed into a new file, the agent id switched (subagent boundary), or a different session object was passed than the one that entered vibe mode.","commonSituations":"Session forking or compaction that replaces the session id/file mid-run; running the exit call from a different agent context than the spawn; resuming a session file from a new path between entry and exit.","solutions":["Perform vibe entry and exit on the same parent session object with an unchanged id, file, and agent id.","If the session was forked, exit vibe mode before forking or re-enter vibe mode in the new session.","Ensure agent id is stable across the tool call (don't run exit inside a subagent whose getAgentId differs from the owner)."],"exampleFix":"// before\nconst scope = registry.currentScope(session);\nawait forkSession(session); // changes sessionId/file\nawait registry.exit(session, scope); // throws\n// after\nawait registry.exit(session, scope);\nawait forkSession(session);","handlingStrategy":"try-catch","validationCode":"const scope = registry.currentScope(session);\nconst now = registry.ownerScope(session);\nif (scope.ownerId !== now.ownerId || scope.parentSessionId !== now.parentSessionId || scope.parentSessionFile !== now.parentSessionFile) {\n  throw new Error(\"Parent session identity changed; re-enter vibe mode before exiting\");\n}","typeGuard":null,"tryCatchPattern":"try {\n  await vibe.exit(session);\n} catch (err) {\n  if (err instanceof ToolError && err.message.includes(\"parent session changed before mode exit\")) {\n    // re-enter vibe mode in the new session scope, then exit there\n    await vibe.enter(session);\n    await vibe.exit(session);\n  } else throw err;\n}","preventionTips":["Never fork/compact or rename sessions while vibe workers are active","Run vibe entry and exit in the same agent context (same ownerId)","Capture and compare the scope before lifecycle operations"],"tags":["session","concurrency","lifecycle"],"backgroundTag":"session-identity-mismatch","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}